/* ==========================================================================
   BLOCKS — section layouts (ANALYSIS §4)
   ==========================================================================
   Rules recap (PRINCIPLES §3):
   - a block owns its vertical padding (.section), never outer margins
   - background comes from a .surface-* class on the instance, never
     from nth-child
   - only the hero carries an H1; every other block opens with H2
   - no hard-coded ids in block markup — anchors come from content
   ========================================================================== */

/* Tighter rhythm for minor strips (partner logos etc.) */
.section--slim {
  padding-block: var(--space-8);
}

/* --------------------------------------------------------------------
   Hero — variant A: centred statement over a full-width photograph

   The hero is the one block that has to say what kind of business this
   is before a word is read, and it is the first thing that tells one
   client's site from another's. So it gets the largest type in the
   system on its own step, the tightest leading, and a photograph at
   full width underneath.

   The type sits above the photo rather than on it. On an abstract
   gradient a headline can be laid over the image and still be read; on
   a photograph of a workshop the contrast changes with every pixel, and
   the only honest ways to fix that are a scrim heavy enough to waste
   the photo or a measurement that stops being true the moment a client
   swaps the picture (PRINCIPLES §11).
   -------------------------------------------------------------------- */
.block-hero__inner {
  position: relative;            /* content sits above the scrim */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.block-hero__title {
  font-size: var(--text-5xl);
  max-width: 18ch;
  margin-block-start: var(--space-6);
}

/* Opening the block itself, with no eyebrow above it, it owns no gap. */
.block-hero__title:first-child {
  margin-block-start: 0;
}

/* --------------------------------------------------------------------
   Hero on a photograph

   The photograph is the loudest thing a theme owns. Three client sites
   built from this base are told apart by it before a visitor reads a
   word, which is why the hero carries it full-bleed rather than as an
   illustration below the text.

   The file comes from the instance as `--section-photo`, the same
   property the photo band uses: the picture is content, not styling.
   -------------------------------------------------------------------- */
.block-hero--photo {
  position: relative;
  display: grid;
  /* The text sits on the block's bottom rhythm and the photograph takes
     whatever is left above it, rather than the two splitting the height
     evenly. Dead centre was the default, not a decision — and it spent
     the room at the top, which is the part of the picture worth seeing.
     Stated as a rule rather than a nudge, so it still holds when the
     headline runs to three lines.

     `safe` matters: where the text is taller than the block — a narrow
     phone — end-alignment would push the first line up off the top and
     behind the header. `safe` gives up and aligns from the top instead.
     The plain `center` above is the fallback for browsers that do not
     know the keyword.

     `--hero-align` and `--hero-foot` are one decision in two declarations,
     the same shape as `--gradient-heading` / `--heading-ink`: change one
     without the other and the result is wrong rather than different. The
     foot's whole meaning is "photograph left clear above the text", which
     only means anything while the text is pinned to the bottom — centre
     the text and that number stops being a measurement and becomes an
     unexplained 64px of bias. A centred hero sets both: `safe center`, and
     a foot equal to the head's own rhythm. What survives is the header
     allowance, and it should: the pill really is sitting in the top 4rem,
     so centring against the space below it is centring against the space
     the reader can see. */
  align-content: center;
  align-content: var(--hero-align, safe end);
  /* 84svh leaves a slice of the next block showing, which is a deliberate
     invitation to scroll — and it is a photograph's logic, where the crop
     is the content and there is no reason to spend the whole screen on it.
     A theme whose hero art is *light* wants the opposite: the glow needs
     the full frame to read as a source rather than as a band. Left as a
     token so both arguments can be right. `svh`, and any value set here
     should be too — see the short-viewport reasoning below. */
  min-height: var(--hero-min-height, 84svh);
  /* Asymmetric on purpose, and the asymmetry is what does the work.

     The top keeps the bar's height on top of the block's own rhythm.
     That is the guard for the overflow case: where the text is taller
     than the block, `safe` alignment gives up and pins it to the top, and
     without this allowance the first line would sit behind the floating
     header.

     The foot is deliberately short. Bottom-aligned content sits exactly
     its bottom padding above the edge, so this number *is* the amount of
     photograph left clear above the text. At a full section's rhythm
     there was six pixels of slack in the whole block and the alignment
     had nothing to move — the text shifted three pixels and looked
     unchanged, because it was. */
  /* ---- Short viewports: the block answers to the shorter axis --------
     Every vertical cost in this hero was sized from the viewport's WIDTH
     — `--section-space` and the type scale are both `vw` clamps — so on a
     screen that is wide but short they all reach their maximum at once and
     the block asks for more height than exists. A laptop is exactly that
     screen: 1440x1080 and 1440x660 got identical type and identical
     padding, and only one of them had the room.

     Measured at 1440 wide, the block demanded 687px whatever the viewport:
     182 of lead-in, 441 of content, 64 of foot. Above about 700px of
     viewport that fits; below it the rounded foot and the page behind it
     are simply off screen, and the hero reads as having no end rather than
     as being tall.

     So each cost is capped against `svh` as well. `min()` rather than a
     breakpoint, so nothing jumps at a threshold — the caps are set to bite
     below roughly 850px of height, which leaves 1920x1080 and every taller
     desktop exactly as it was and lets the laptops down gently.

     `svh` and not `vh` to match the `min-height` above: the small viewport
     is the stable one, so a mobile browser hiding its chrome mid-scroll
     cannot resize the type under the reader.

     The header allowance is deliberately NOT capped. It is the guard that
     keeps the first line clear of the floating bar when `safe` alignment
     gives up and pins the text to the top, and a guard that shrinks when
     space is tight is no guard at all. */
  padding-block:
    calc(min(var(--section-space), 14svh) + var(--header-height))
    var(--hero-foot, min(var(--space-16), 8svh));
  color: var(--color-text-on-inverse);
  /* The photograph moved to its own layer so it can be moved; that layer
     is oversized and has to be cut back to the panel's corners, which a
     border-radius alone does not do to a transformed child. */
  overflow: hidden;
  /* And the layer sits at `z-index: -1` to get underneath the scrim.
     Without a stacking context of its own here, that -1 would send it
     behind this block's ancestors instead — out through the back of the
     page, where it is not so much below the scrim as gone. */
  isolation: isolate;
}

/* The photograph, on a layer of its own.

   It arrives a touch too large and settles back over two seconds — long
   enough to be felt rather than watched, which is the only speed at
   which an entrance like this is not a trick.

   Expo-out spends most of that time almost still: the picture has done
   the bulk of its travel early and the last stretch is barely moving.
   That tail is why nothing needs to happen after it. */
.block-hero--photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--section-photo);
  background-size: cover;
  background-position: center;
  /* Two animation slots, and they are not interchangeable.

     `--hero-entrance` REPLACES the settle. A picture arriving a touch too
     large and easing back is one way for art to appear and it is not the
     only one; a theme whose art is light may want it to *switch on*
     instead. It has to be a replacement rather than an addition because
     both own `scale`, and two animations fighting over one property is a
     bug that only shows on slow machines. Whatever a theme puts here it
     inherits the settle's duty: end at rest, in one pass, and never run
     again (PRINCIPLES §12).

     `--hero-ambient` is the second, additive slot — for art that must not
     go completely still, because light that never moves reads as a printed
     gradient. It runs alongside whichever entrance is playing, so it must
     animate something the entrance does not touch; opacity is the safe one.

     The theme supplies both names and its own `@keyframes` (a theme file is
     a stylesheet like any other). Kept to the same rule as everything else
     here (PRINCIPLES §12): all of this is decoration, the art is already
     there without it, and the reduced-motion block below turns it off. */
  animation: var(--hero-entrance, hero-photo-settle 2s var(--ease-out-expo) both),
             var(--hero-ambient, none);
}

@keyframes hero-photo-settle {
  from { scale: 1.06; }
  to   { scale: 1; }
}

/* The scrim's own slot, and it exists because of what the scrim is FOR.

   It is a contrast device: it costs the art brightness so the text on top
   of it can be read. During an entrance there is no text on top of it yet
   — the reveal cascade has not started — so a scrim at full strength in
   those first few hundred milliseconds is a tax paid for nothing, dimming
   the one moment the art has the screen to itself.

   An entrance that opens the art therefore usually wants this too: the
   scrim arrives as the content does, and by the time the first line of the
   headline is readable it is at exactly the strength that was measured for
   it. Optional and default `none`, so the settle keeps its scrim from the
   first frame, which is right for a picture that is never fully hidden.

   The end state is not negotiable: whatever is written here must finish at
   `opacity: 1`, with `both`, or the block ships with an unmeasured scrim
   and PRINCIPLES §11 is quietly gone. */
.block-hero--photo::before {
  animation: var(--hero-scrim-entrance, none);
}

/* The content waits for the frame.

   An entrance that has to open before there is anything to read needs the
   reveal cascade to hold for its own length, and `--reveal-lead-in`
   (motion.css) is how a subtree asks for that. Published HERE rather than
   in the theme's `:root` on purpose: set on the page it would delay every
   reveal in every block, and the hero's entrance is no reason for the
   footer to arrive late.

   The theme sets `--hero-lead-in` to match the keyframes it wrote — one
   number, stated next to them. Unset, nothing waits and the cascade is
   exactly what it always was.

   Published under `.has-reveal` and not on the block outright, which is
   the difference between a delay and a blank screen. Without reveals —
   reduced motion, no JS, an old browser — there is no cascade to hold and
   nothing to hold it for, so the number must not survive: anything reading
   it as a start time would sit at zero opacity through a lead-in that is
   waiting for an entrance nobody is going to see. */
.has-reveal .block-hero--photo {
  --reveal-lead-in: var(--hero-lead-in, 0s);
}

/* An entrance is decoration by definition — the picture is already there
   without it (PRINCIPLES §12). Both layers, or a scrim animating in over
   art that never hid would be a fade nobody asked for. */
@media (prefers-reduced-motion: reduce) {
  .block-hero--photo::after,
  .block-hero--photo::before {
    animation: none;
  }
}

/* The scrim, shaped to the text rather than to the picture.

   A flat wash has to be as dark as its most demanding element needs,
   which meant 68% everywhere and a photograph you could barely see. But
   the demand is not uniform: an 80px headline clears AA at 3:1 and needs
   only ~52% of scrim, while the 14px guarantees under the buttons need
   4.5:1 and ~66%. The type is stacked, so the requirement is stacked
   too — light where the headline is, firm where the small print is.

   The stops are placed against measured bands, not by eye:
     33–56%  headline     (80px — large text, 3:1)   → 55%, measures 3.43
     59–72%  lead         (20px — 4.5:1)             → 72%, measures 5.43
     88–92%  guarantees   (14px — 4.5:1)             → 79%, measures 10.37

   Re-measure after ANY change to the block, not just to the picture.
   Moving the text down to give the photograph more room at the top slid
   the lead over a brighter patch and dropped it to 4.30 — a fail, from a
   change that had nothing to do with colour. The ramp starts ten points
   earlier because of it.

   Re-measure whenever the photograph changes: the numbers belong to the
   image, not to the stylesheet (PRINCIPLES §11). A darker, lower-key
   commissioned shot would let every one of these come down. */
.block-hero--photo::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;    /* the scrim has to follow the panel's corners */
  /* `--hero-scrim` because the numbers belong to the image (the rule two
     paragraphs up), and the image comes from the instance — so when the
     art changes radically, its scrim must be allowed to travel with it.
     The default is the ramp measured for the first client's photograph.
     The Ingrade hero is the case that forced this: its art is a glow whose
     whole point is a bright horizon at the foot, exactly where a
     photograph's scrim must be firmest — same block, same text bands,
     opposite demand profile. Whoever sets this token inherits the duty
     the default carries: measure the text bands, not the mood. */
  background: var(--hero-scrim, linear-gradient(
    to bottom,
    color-mix(in srgb, var(--color-surface-inverse) 55%, transparent) 0%,
    color-mix(in srgb, var(--color-surface-inverse) 55%, transparent) 40%,
    color-mix(in srgb, var(--color-surface-inverse) 70%, transparent) 50%,
    color-mix(in srgb, var(--color-surface-inverse) 82%, transparent) 100%
  ));
}

/* The type shrinks on a short screen for the same reason the padding does
   — see the block's own rule above. Two lines of 80px display is 173px of
   the 660 a laptop has, and it is the single largest thing that can give.

   Capped rather than rescaled: `min()` against the height leaves the width
   clamp in charge everywhere it already looked right, and only takes over
   once the screen is the thing running out. Above ~840px of height these
   resolve to exactly the tokens they always did. */
.block-hero--photo .block-hero__title {
  font-size: min(var(--text-5xl), 9.5svh);
}

.block-hero--photo > .container > .lead {
  font-size: min(var(--text-xl), 3svh);
}

/* A shadow the reader never notices, doing the work when a client swaps
   in a photograph brighter than this one. It is insurance on top of the
   scrim, not a substitute for measuring. */
.block-hero--photo .block-hero__title,
.block-hero--photo > .container > .lead {
  text-shadow: 0 4px 24px color-mix(in srgb, var(--color-surface-inverse) 60%, transparent);
}

.block-hero--photo .lead,
.block-hero--photo .hero-highlights li,
.block-hero--photo .hero-highlights .icon {
  color: var(--color-text-on-inverse);
}

/* The eyebrow's surface and border are built for a light page; on the
   photograph it becomes a tinted chip of the same dark the scrim uses,
   so it reads as part of the image rather than as a sticker on it. */
.block-hero--photo .eyebrow {
  border-color: color-mix(in srgb, var(--color-text-on-inverse) 30%, transparent);
  background: color-mix(in srgb, var(--color-surface-inverse) 45%, transparent);
  color: var(--color-text-on-inverse);
}

/* The ghost button is drawn in the brand colour, which on a dark
   photograph is a brown word on a brown room. */
.block-hero--photo .btn--ghost {
  color: var(--color-text-on-inverse);
}

/* …and the hover reads through the theme's slots before falling back to
   that same reasoning. This rule is more specific than the component's
   (0,3,0 against 0,2,0), so without the fallback chain it would silently
   win over any ghost hover a theme defines — the trap this file has
   recorded twice already under "context selectors quietly eat components".
   What it must keep owning is the *photograph* case: a theme that says
   nothing still gets a wash the label survives on top of a picture. */
.block-hero--photo .btn--ghost:hover {
  background: var(--btn-ghost-hover-surface,
    color-mix(in srgb, var(--color-text-on-inverse) 15%, transparent));
  color: var(--btn-ghost-hover-ink, var(--color-text-on-inverse));
}

/* The floating header opens space for itself at the top of the page,
   which here would sit the hero panel a full block-rhythm down the
   screen with a band of surface colour above it. The panel takes over:
   it opens the same gutter it keeps at its sides, and the pill floats
   over its top edge — which is the point of a floating header. */
body:has(.site-header--floating:not([hidden], .pv-off)) main:has(> .block-hero--photo) {
  padding-block-start: 0;
}

/* The photograph runs to the very top of the screen and squares its top
   corners, so the floating pill sits *inside* the image instead of
   straddling its edge. A bar half on the photo and half on the page
   below it reads as two things that failed to line up, and the rounded
   corner passing behind it makes that worse.

   The alternative was to push the pill down on this page only, but then
   the header sits at one height on the homepage and another on every
   subpage — the bar would jump as a visitor moves through the site. The
   picture moving is free; the navigation moving is not.

   On a phone it goes edge to edge and drops the rounding entirely. A
   12px gutter either side of a photograph is not an inset, it is a
   hairline of page showing past the picture — and a corner rounding away
   from a gap that thin has nothing to round against. The panel language
   only has room to mean something once the screen is wide enough for the
   gutter to read as one. */
.block-hero--photo {
  margin-block-start: 0;
  margin-inline: 0;
  border-radius: 0;
}

@media (min-width: 48rem) {
  .block-hero--photo {
    /* Follows the panel language, so a full-bleed theme takes the hero
       with it — see `--panel-inset` in components.css. */
    margin-inline: var(--panel-inset, var(--space-6));
    /* Top corners stay square so the floating pill sits inside the
       picture; the bottom pair ties the block to the panels below it. */
    border-end-start-radius: var(--radius-panel);
    border-end-end-radius: var(--radius-panel);
  }
}

.block-hero__inner > .lead {
  margin-block-start: var(--space-6);
  /* Narrower than the reading measure. Centred text is scanned, not
     read: past a couple of lines the ragged left edge starts costing
     more than the symmetry gains. */
  max-width: 46ch;
}

.block-hero__inner .hero-actions,
.block-hero__inner .hero-highlights {
  justify-content: center;
}

/* The split hero (text | media side by side) is gone rather than kept
   as a variant: no page renders it, and a variant nothing renders cannot
   be checked while building a theme, which makes it a liability rather
   than an option (PRINCIPLES §10). It comes back with markup the day
   there is a client it suits. */

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-block-start: var(--space-8);
}

.hero-highlights {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  margin-block-start: var(--space-8);
  padding: 0;
  list-style: none;
}

.hero-highlights li {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--font-weight-bold);
  color: var(--color-text-muted);
}

.hero-highlights .icon {
  width: 1.25rem;
  height: 1.25rem;
  color: var(--color-primary);
}


/* --------------------------------------------------------------------
   About — text + media, mirrorable
   -------------------------------------------------------------------- */
.block-about__grid {
  display: grid;
  gap: var(--space-10);
  align-items: center;
}

@media (min-width: 56rem) {
  .block-about__grid {
    grid-template-columns: 0.9fr 1.1fr;
    gap: var(--space-16);
  }

  .block-about--mirror .block-about__media {
    order: 2;
  }
}

.block-about__media .img-frame {
  border-radius: var(--radius-xl);
}

.about-stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin-block-start: var(--space-8);
  padding: 0;
  list-style: none;
}

/* Big numbers are emphasis, not action — the natural job of a second
   brand colour, and large enough that AA needs 3:1 rather than 4.5:1.

   That last clause used to end the comment, and it was the wrong question
   answered confidently: the rule worked out *what* the number had to clear
   and never asked whether a second brand colour can clear it. drga's sage
   measures 5.32 on its own surface, so it passed and nobody looked. The
   first yellow brand measures 1.85 — a number the reader cannot see at all.

   `--color-secondary-strong` is the same colour asked to be read rather
   than seen. A theme whose mark already reads points it at the same value
   and nothing on the page moves. */
.stat__value {
  display: block;
  font-family: var(--font-heading);
  font-weight: var(--font-weight-heading);
  /* Slotted: a number is emphasis, and how loud emphasis speaks is the
     theme's voice — the reference language sets its numbers at display
     size and lets them carry the block. Default stays the quiet 2xl. */
  font-size: var(--stat-size, var(--text-2xl));
  line-height: 1.1;
  color: var(--color-secondary-strong);
}

/* A dark slab inverts the problem rather than repeating it: the value that
   reads on a light surface is the one that vanishes here, and the mark
   colour that vanished there is the one that reads — 2.15 against 7.95 on
   the dark slab of the theme that exposed it, exactly the wrong way round.
   The gold theme meets the same thing from the other side: a gold number
   on a gold CTA band.

   No third and fourth secondary token for that. A block's background is an
   instance property, so any block can be put on any surface, and a token
   set that needs one value per surface per role is a token set nobody can
   fill in. The number takes the surface's own text colour instead:
   emphasis gives way to legibility, and both of these are required tokens
   that every theme has already had to verify. */
.surface-inverse .stat__value { color: var(--color-text-on-inverse); }
.surface-brand   .stat__value { color: var(--color-text-on-primary); }

.stat__label {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* `.grid-2` is gone. Its only user was the testimonials block, which is now
   a `.carousel__track` — and the track answers the lone-item case for free:
   one card takes one column of the two the row is measured in, and sits
   where the first of a pair would. Left, aligned with the heading above it,
   which is where it belongs; centred it read as a third alignment against an
   eyebrow and a heading that are both flush left. */

/* --------------------------------------------------------------------
   Process — numbered steps on a shared timeline

   How a collaboration runs, start to finish: each step carries a pin
   with an icon, a short milestone label with a deadline, and a numbered
   title with its explanation. One data shape; the connector between the
   pins is drawn by the block, not by the data.

   The connector is ornament, so its colour owes nothing to AA — it mixes
   the secondary toward the surface the way a 300 step would. The pin's
   icon is a mark, which is the secondary's actual job; the deadline is
   metadata and reads muted. Counters number the titles so a reordered
   step renumbers itself — a typed "3." would survive the reorder wrong.
   -------------------------------------------------------------------- */
.process {
  --process-pin: 3rem;
  display: grid;
  gap: var(--space-10) var(--space-8);
  padding: 0;
  margin: 0;
  list-style: none;
  counter-reset: process;
}

.process-step {
  position: relative;
  display: grid;
  grid-template-columns: var(--process-pin) 1fr;
  grid-template-rows: auto auto auto;
  column-gap: var(--space-4);
  align-content: start;
}

.process-step__pin {
  grid-row: 1 / span 3;
  width: var(--process-pin);
  height: var(--process-pin);
  display: grid;
  place-items: center;
  background: var(--color-surface);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  color: var(--color-secondary);
}

.process-step__pin .icon {
  width: 1.5rem;
  height: 1.5rem;
}

.process-step__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
}

.process-step__label {
  font-size: var(--text-sm);
  font-weight: var(--font-weight-bold);
  color: var(--color-secondary-strong);
}

/* Same escape the stat value takes: on a dark or brand slab the mark
   colour that read on the light surface is the one that vanishes, and
   the surface's own text colour is the only value already verified. */
.surface-inverse .process-step__label { color: var(--color-text-on-inverse); }
.surface-brand   .process-step__label { color: var(--color-text-on-primary); }

.process-step__duration {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.process-step__title {
  font-size: var(--text-lg);
  margin-block-start: var(--space-2);
}

.process-step__title::before {
  counter-increment: process;
  content: counter(process) ". ";
}

.process-step__text {
  font-size: var(--text-base);
  color: var(--color-text-muted);
  margin-block-start: var(--space-2);
}

/* The connector. On mobile the steps stack and the line runs down
   through the pins; the segment belongs to the step above it, so the
   last step draws none and the line ends where the process does. */
.process-step:not(:last-child)::before {
  content: "";
  position: absolute;
  left: calc(var(--process-pin) / 2);
  top: calc(var(--process-pin) + var(--space-1));
  bottom: calc(var(--space-1) - var(--space-10));
  border-inline-start: 2px dashed
    color-mix(in srgb, var(--color-secondary) 35%, var(--color-surface));
}

@media (min-width: 56rem) {
  .process {
    grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  }

  .process-step {
    grid-template-columns: 1fr;
    row-gap: var(--space-3);
  }

  .process-step__pin {
    grid-row: auto;
  }

  .process-step__head { margin-block-start: var(--space-1); }
  .process-step__title { margin-block-start: 0; }
  .process-step__text { margin-block-start: 0; }

  /* Sideways now: from the right edge of the pin into the column gap,
     stopping a breath short of the next pin. */
  .process-step:not(:last-child)::before {
    left: calc(var(--process-pin) + var(--space-3));
    right: calc(var(--space-3) - var(--space-8));
    top: calc(var(--process-pin) / 2);
    bottom: auto;
    border-inline-start: none;
    border-block-start: 2px dashed
      color-mix(in srgb, var(--color-secondary) 35%, var(--color-surface));
  }
}

/* --------------------------------------------------------------------
   CTA band
   -------------------------------------------------------------------- */
.block-cta__inner {
  max-width: 46rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-6);
  text-align: center;
}

/* The band measured 684px against the hero's 749 — 91% of it — and read
   as a second hero rather than as a prompt on the way past. The height
   was almost entirely padding: 119px top and bottom, the full section
   rhythm, around content that is four short elements.

   Only the padding is trimmed. The heading was the obvious second
   suspect and turned out to be innocent — it is already `--text-4xl`
   against the hero's `--text-5xl`, a genuine step down, and cutting it
   further would have changed the band's character rather than its size.
   0.72 lands it at 618px, 82% of the hero: still substantial, no longer
   arguing with the top of the page. */
.section:has(> .block-cta__inner) {
  padding-block: calc(var(--section-space) * 0.72);
}

/* A block can carry a photo behind its surface colour. The photo is
   content, so the file comes from the instance:
     <section class="section--photo" style="--section-photo: url(…)">

   This used to be a 92% brand wash — the photograph was technically
   present and practically invisible, and the band read as one loud slab
   of colour. It now works the way the hero does: the photograph is the
   surface, and a scrim over it buys the contrast. The tint is
   `--color-primary-900` rather than a neutral black, so the band still
   belongs to the brand; it is the darkest step of the ramp, which is
   what makes a moderate percentage enough.

   `.surface-brand` still sets the background *colour* underneath, so a
   block whose photo is missing or fails to load degrades to the flat
   brand slab rather than to unreadable text (PRINCIPLES §8). */
.section--photo {
  position: relative;
  background-image: var(--section-photo);
  background-size: cover;
  background-position: center;
}

.section--photo::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;   /* the scrim must follow a panel's corners */
  /* Measured against the brightest pixel under each text band of the
     actual rendered crop, not the source file. Lighter through the
     middle, where the display heading sits and large text only needs
     3:1; closing down top and bottom where the eyebrow and the lead are
     small.

     These numbers are not tuned to the photograph currently in the CTA —
     they are the lightest scrim that clears AA on **all six** photos in
     the library, found by binary search. Tuned to one picture the block
     would look best today and break the first time a client swapped the
     file, which is not a template. Tightest margin is 0.33 over AA
     (`dilna-hero`, lead at 4.83); the current CTA photo sits at
     6.12 / 6.24 / 6.63.

     There is a real ceiling here and it belongs to the picture, not the
     CSS: a photo with large near-white areas cannot be shown much behind
     white text however the scrim is drawn. Lightening this to where wood
     grain really sings put four of the six photos under AA — which is
     why the strength is set by the whole library and not by whichever
     picture a block happens to be carrying. Any new photo is re-measured
     against its own bright areas (PRINCIPLES §11).

     `--section-scrim` is the same escape the hero has: the numbers belong
     to the art, and the art comes from the instance. A block lit by a
     gradient rather than covered by a photograph needs the opposite
     curve — the light is the point and a 74% veil erases it — so a theme
     whose sections are pools of light supplies its own. Whoever sets it
     inherits the duty above: measure the text bands, not the mood. */
  background: var(--section-scrim, linear-gradient(
    to bottom,
    color-mix(in srgb, var(--color-primary-900) 74%, transparent) 0%,
    color-mix(in srgb, var(--color-primary-900) 62%, transparent) 38%,
    color-mix(in srgb, var(--color-primary-900) 66%, transparent) 62%,
    color-mix(in srgb, var(--color-primary-900) 78%, transparent) 100%
  ));
}

/* Insurance for the day a client swaps in a brighter photograph than the
   one this was measured against. Not a substitute for measuring — the
   same pairing the hero uses. */
.section--photo :where(h2, .display, .lead) {
  text-shadow: 0 2px 18px color-mix(in srgb, var(--color-primary-900) 55%, transparent);
}

.section--photo > * {
  position: relative;       /* content sits above the wash */
}

/* Buttons sitting on the brand surface invert to stay visible: the button
   becomes a piece of the page laid on the brand slab. Which is exactly why
   its label is the page's own ink — `--color-primary-800` was an ink chosen
   to read on the ramp's light tints, and on a dark theme it sat at 1.9:1 on
   the button's own black. `--color-text` is the one colour every theme has
   already had to make legible against `--color-surface`, which is what this
   button is made of. Same for the hover, which now only tints.

   Slotted since the first theme whose filled button is NOT the brand
   colour (vitarent: yellow CTAs over a violet primary). For that theme
   the inversion solves a collision that never happens and costs the CTA
   its loudest placement — the yellow button on the violet band. The
   escape stays the default; a theme whose `--btn-surface` already
   contrasts with the brand slab points these at its own button tokens
   and keeps its button. The duty travels with the choice: the pairing
   is the theme's to measure, exactly as with `--btn-surface` itself. */
.surface-brand .btn--primary {
  background: var(--btn-brand-surface, var(--color-surface));
  color: var(--btn-brand-ink, var(--color-text));
}

.surface-brand .btn--primary:hover {
  background-color: var(--btn-brand-hover-surface,
    color-mix(in srgb, var(--color-primary) 12%, var(--color-surface)));
  color: var(--btn-brand-hover-ink, var(--btn-brand-ink, var(--color-text)));
}

/* The ghost has the same problem one line quieter: its resting ink IS
   the brand colour, so on the brand slab the label vanishes outright —
   found on the calculator's "Zpět", violet on violet. The slab's
   on-primary ink is the one colour every theme has already measured
   against this surface, and the hover becomes that ink as a wash,
   because the tint recipe the ghost normally mixes assumes the page
   surface underneath. */
.surface-brand .btn--ghost {
  color: var(--color-text-on-primary);
}

.surface-brand .btn--ghost:hover {
  background: color-mix(in srgb, var(--color-text-on-primary) 14%, transparent);
  color: var(--color-text-on-primary);
}

/* --------------------------------------------------------------------
   Contact — form | info card
   -------------------------------------------------------------------- */
.block-contact__grid {
  display: grid;
  gap: var(--space-10);
  align-items: start;
}

@media (min-width: 56rem) {
  .block-contact__grid {
    grid-template-columns: 1.15fr 0.85fr;
    gap: var(--space-16);
  }

  /* A contact block without its aside is a form on its own, and a form
     stretched across a wide container is a form nobody wants to fill in —
     the labels end up a screen away from the fields they belong to. The
     layout reads the count rather than being told (PRINCIPLES §9, same
     idiom as the card grid): one child, one column, held to a width a
     form is comfortable at and centred, because a lone column parked at
     the leading edge reads as the other one having failed to load. */
  .block-contact__grid:has(> :only-child) {
    grid-template-columns: min(100%, 40rem);
    justify-content: var(--contact-solo-align, start);
  }
}

.contact-info {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-8);
  background: var(--color-surface-alt);
  border-radius: var(--radius-lg);
}

.contact-info__row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.contact-info__row .icon {
  flex-shrink: 0;
  color: var(--color-primary);
  margin-block-start: 0.15em;
}

.contact-info__note {
  padding-block-start: var(--space-4);
  border-block-start: var(--border-width) solid var(--color-border);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* --------------------------------------------------------------------
   Contact cards — one channel per card

   The homepage keeps `.contact-info`: one panel beside the form, where
   the block has a column to fill and the page is not about contacting
   anyone yet. The contact page splits the same content into a card each,
   because there the channels *are* the content — a phone number that has
   to be found inside a paragraph is a phone number nobody rings.

   Two layouts, and the choice is not cosmetic. A row stretches every card
   to the tallest of them, which is right when they carry comparable
   amounts and wrong the moment one does not: a phone number sitting under
   a quarter of an empty card reads as a card that lost its content. Beside
   a form, stack them — then each card is exactly as tall as what it holds.
   -------------------------------------------------------------------- */
.contact-cards {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}

.contact-cards--stack {
  grid-template-columns: 1fr;
}

.contact-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-6);
  background: var(--color-surface-alt);
  border-radius: var(--radius-lg);
}

/* A card takes its surface from whatever it was dropped onto: the alt
   tone on the plain page, the plain tone on any panel that already has a
   tone of its own. Two beiges touching is not a card, it is a smudge —
   and on the dark surfaces the text has to come back to its own colour,
   or a light card inherits the panel's light type. */
.surface-alt .contact-card,
.surface-brand .contact-card,
.surface-inverse .contact-card {
  background: var(--color-surface);
  color: var(--color-text);
}

/* A bare icon, the way the homepage's info rows carry theirs. It had a
   tinted tile behind it and that was a second surface inside a surface —
   a panel drawn on a panel for no reason the content asked for. */
.contact-card__icon {
  display: flex;
  margin-block-end: var(--space-2);
  color: var(--color-primary);
}

/* Not `.eyebrow` — that component is a bordered pill with a dot, which
   inside a card reads as a second card. Same typographic idea, same
   0.04em, without the chrome. */
.contact-card__label {
  font-size: var(--text-xs);
  font-weight: var(--font-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-text-muted);
}

/* The value is the point of the card, so it is the one line set larger
   than body text — and a link wherever the channel can be opened. */
.contact-card__value {
  font-size: var(--text-lg);
  font-weight: var(--font-weight-bold);
  line-height: var(--leading-tight);
}

.contact-card__note {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* What a business has to publish, kept apart from the address a visitor
   reads — same content, two audiences. Pushed to the bottom edge, so it
   sits on the card's last line however tall the card has to be. */
.contact-card__legal {
  margin-block-start: auto;
  padding-block-start: var(--space-4);
  border-block-start: var(--border-width) solid var(--color-border);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--color-text-muted);
}

/* --------------------------------------------------------------------
   Contact and Location — two blocks, one grid

   Contact asks "get in touch": the form, with the channels that reach a
   person directly beside it. Location answers "where do I find you": the
   map, its route button, and the address. Named for what they do rather
   than for this client's workshop — a showroom, a surgery and a pick-up
   point are the same block.

   Either can be moved or switched off, but they share the column ratio on
   purpose: when they do sit together the map lands under the form and the
   address under the channels, and the page reads as one layout, not two.
   -------------------------------------------------------------------- */
.block-contact__split,
.block-location__grid {
  display: grid;
  gap: var(--space-10);
  align-items: start;
}

@media (min-width: 56rem) {
  .block-contact__split,
  .block-location__grid {
    grid-template-columns: 1.15fr 0.85fr;
    gap: var(--space-16);
  }
}

/* The address card is as tall as the map beside it, which is what gives
   `__legal` its bottom edge to sit on. */
.contact-card--location {
  height: 100%;
}

.contact-map {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* The button belongs to the map above it, so it takes the map's width
   rather than sitting under one corner of it. */
.contact-map .btn {
  align-self: stretch;
}

/* --------------------------------------------------------------------
   Partners — label beside an endlessly drifting row of logos
   -------------------------------------------------------------------- */
.block-partners__grid {
  display: grid;
  gap: var(--space-6);
  align-items: center;
}

@media (min-width: 56rem) {
  .block-partners__grid {
    grid-template-columns: auto 1fr;
    gap: var(--space-12);
  }
}


.block-partners__label {
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--color-text-muted);
  text-wrap: balance;
}

.partner-logo {
  font-family: var(--font-heading);
  font-size: var(--text-lg);
  color: var(--color-text-muted);
  opacity: 0.65;
  white-space: nowrap;
}

/* --------------------------------------------------------------------
   Marquee — an endless drifting row
   -------------------------------------------------------------------- */
.marquee {
  overflow: hidden;
  /* Fade the ends with a mask, not with gradient overlays painted in the
     section's background colour. A mask makes the pixels genuinely
     transparent, so the effect survives a theme changing the surface,
     the block moving onto a panel, or a photo appearing behind it —
     none of which a colour-matched gradient would survive. */
  -webkit-mask-image: var(--marquee-fade);
  mask-image: var(--marquee-fade);
  --marquee-fade: linear-gradient(
    to right,
    transparent,
    #000 var(--space-16),
    #000 calc(100% - var(--space-16)),
    transparent
  );
}

.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee-drift var(--marquee-duration, 44s) linear infinite;
  will-change: transform;
}

/* Spacing lives on the item, never as `gap` on the track. With `gap`,
   n items produce n-1 gaps, so two copies are not exactly twice one
   copy and the -50% loop point lands mid-gap — a visible stutter every
   cycle. Padding on each item makes every item exactly the same width,
   and the loop is seamless. */
.marquee__item {
  padding-inline-end: var(--space-12);
}

/* Wrapping the duplicate set keeps it out of the accessibility tree
   while `display: contents` keeps it out of the layout. */
.marquee__duplicate {
  display: contents;
}

/* The strip does not pause on hover. Pausing is for things a reader
   might want to stop and act on — these are supplier names, nothing here
   is clickable, and there is no reason to hover it on purpose. All the
   pause did was stall the drift whenever the pointer crossed the strip on
   its way somewhere else, which reads as the page snagging. */

@keyframes marquee-drift {
  to {
    transform: translateX(-50%);
  }
}

/* --------------------------------------------------------------------
   FAQ
   -------------------------------------------------------------------- */
.block-faq__inner {
  max-width: 46rem;
}

/* --------------------------------------------------------------------
   Text page — GDPR, terms, cookies, contest rules
   A single reading column. The page title sits apart from the body so
   long documents open with a clear head rather than a wall of text.

   Centred, like the article's. Pinned left it left 16px of margin on one
   side and 607px on the other, and asymmetric emptiness reads as a
   layout that failed rather than one that was chosen — there is nothing
   on the right for the column to be making room for. It also kept the
   privacy page's text in a different place from the article's, so
   following a link from one to the other made the words jump sideways.
   -------------------------------------------------------------------- */
.block-text__head,
.block-text__body {
  max-width: var(--measure);
  margin-inline: auto;
}

.block-text__head {
  margin-block-end: var(--space-12);
}

.block-text__meta {
  margin-block-start: var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* --------------------------------------------------------------------
   404 — the one page nobody meant to open
   Centred and short. Every element on it points somewhere useful: a
   dead end is the one place a site must not also be a cul-de-sac.
   -------------------------------------------------------------------- */
.block-notfound {
  position: relative;
  overflow: hidden;              /* clips the oversized drawing */
  /* The drawing sits at `z-index: -1` to get under the words. Without a
     stacking context of its own here that -1 would send it behind this
     block's ancestors instead — out through the back of the page. Same
     pairing, and the same reason, as the hero's photograph. */
  isolation: isolate;
}

/* The drawing takes the place the "404" numeral used to hold: oversized,
   centred, behind the words. It replaced that numeral because the page
   was showing the same watermark trick twice — this block's and the
   footer's — and a drawing says what the numeral said without repeating
   the device.

   Behind rather than above. Set in the flow it pushed the whole block
   taller for a picture nobody came to look at; behind, the page keeps the
   short, mostly-empty shape a 404 wants and the drawing is atmosphere.

   The drawing is the theme's, not the template's — the first block whose
   *content* varies by client rather than by instance. A carpenter gets
   tools; the next brand gets whatever its own trade looks like.

   Faded hard, and the number is measured rather than chosen: the words on
   top are the block's whole job, so the picture may only take contrast it
   can spare. See the figure in the CHANGELOG. */
.block-notfound__art {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
  width: min(120%, 60rem);
  height: auto;
  opacity: 0.22;
  pointer-events: none;
  user-select: none;
  /* Under the words, and this is the whole trick. An absolutely positioned
     element paints *above* every in-flow sibling regardless of DOM order —
     so without this the drawing came through the solid CTA button. It is
     not a stacking-order preference, it is the rule. */
  z-index: -1;
}

/* Nothing on this block can afford muted grey, because everything on it
   sits over a picture. Measured against the darkest ink beneath each one,
   the lead came out at 3.48 and the line above the links at 3.67 — both
   under the 4.5 they owe as normal text. At full text colour the same
   worst cases are 9.0 and 8.7.

   Fading the drawing far enough to rescue the grey was the alternative and
   it costs more than it saves: the picture would have to drop to about
   0.08, which is not a picture, it is a smudge. Everything else already
   passes at 0.22 — heading 8.9, eyebrow 7.2, links 4.5 to 5.3.

   The hierarchy survives without the grey. It was never carrying it alone:
   68px, 20px and 14px say the same thing louder than a tint does.

   Scoped to this block, and it has to be — the thank-you page renders the
   same component with no drawing behind it, and there the muting is doing
   its ordinary job. */
.block-notfound__art ~ * .lead,
.block-notfound__art ~ * .block-notfound__links p {
  color: var(--color-text);
}

.block-notfound__inner {
  max-width: 40rem;
  margin-inline: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  text-align: center;
}

.block-notfound__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-3);
  margin-block-start: var(--space-4);
}

/* The links are the block's actual job — getting the visitor back into
   the site. Kept quiet: a second row of buttons would compete with the
   two real actions above instead of supporting them. */
.block-notfound__links {
  width: 100%;
  margin-block-start: var(--space-8);
  padding-block-start: var(--space-6);
  border-block-start: var(--border-width) solid var(--color-border);
}

.block-notfound__links p {
  margin-block-end: var(--space-4);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.block-notfound__links ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2) var(--space-6);
  padding: 0;
  list-style: none;
}

/* --------------------------------------------------------------------
   Thank you — the page after the form

   Shares the 404's centred shell: both are a single short block whose
   job is to tell someone where they now stand. Only the parts that
   differ live here.
   -------------------------------------------------------------------- */
/* A tick, not an illustration. The reader wants confirmation in the
   first glance and nothing else in it — the message underneath does the
   rest, and a drawing here would be the site congratulating itself. */
.block-thanks__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 4rem;
  height: 4rem;
  margin-block-end: var(--space-6);
  border-radius: var(--radius-full);
  background: var(--color-success-soft);
  color: var(--color-success);
}

.block-thanks__mark .icon {
  width: 2rem;
  height: 2rem;
}

/* What happens next, in the order it will happen. Left-aligned inside a
   centred block on purpose: it is the one part of the page that is read
   rather than glanced at, and three centred lines of different lengths
   are harder to read than they look. */
.block-thanks__next {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: fit-content;
  margin-block-start: var(--space-8);
  padding: 0;
  list-style: none;
  text-align: start;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.block-thanks__next li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

.block-thanks__next .icon {
  width: 1.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  color: var(--color-primary);
  /* Nudged onto the text's first baseline rather than the line box's
     top, which is an optical fix and so allowed to be an em. */
  margin-block-start: 0.1em;
}

/* --------------------------------------------------------------------
   Footer — centred, with a wordmark watermark
   -------------------------------------------------------------------- */
.site-footer {
  position: relative;
  overflow: hidden;              /* clips the oversized watermark */
  /* Tighter at the foot than at the head. The signature is the last
     thing on the page and wants closing off, not a second helping of the
     space that opens the footer — 64px below a 16px-tall mark left it
     floating with the page ending somewhere underneath it. Matches the
     32px above it instead. */
  padding-block: var(--space-16) var(--space-8);
  /* Not a flat wash. A solid footer meets the block above it on a hard
     horizontal line, and at the end of a page that line reads as the page
     being cut off rather than finished. Fading the colour in from the top
     removes the seam entirely: the previous block's surface simply carries
     on and deepens toward the foot, so the page closes instead of stopping.

     Full height rather than a short fade over the top padding — a smudge
     across the first 60px of a 400px footer looks like a rendering
     artefact, while the whole height reads as a decision.

     The far end is the plain `transparent` keyword and that is deliberate.
     It resolves to `rgb(0 0 0 / 0)`, which in the old days greyed the middle
     of a fade; gradients are now interpolated premultiplied, so the hue is
     carried by the opaque stop and no grey appears. Spelling it as
     `color-mix(… , transparent)` would be worse, not better: with no
     percentage that is a 50/50 mix — half-opaque, not clear — and at `0%` it
     is transparent *black*, which is the very thing being avoided.

     Works whatever sits above it, which is the point (PRINCIPLES §8 and the
     reorderable-blocks rule): against `surface-base` it dissolves, against a
     `surface-alt` panel there was never a seam to hide.

     Optionally the theme's own art: `--footer-backdrop` replaces the fade
     wholesale — a glow rising under the end of the page is how the
     reference language closes a site. The theme takes over the *whole*
     image stack, fade included, so the two cannot half-combine.

     ⚠ The audit cannot see a background-image, so a theme that sets this
     measures by hand: the footer's small print sits inside whatever is
     drawn here, and the peak of a glow has to stay below what 4.5:1 allows
     under muted text — that is the constraint that shapes the art, not
     taste. */
  background-image: var(--footer-backdrop, linear-gradient(
    to top,
    var(--color-surface-alt),
    transparent
  ));
  /* A travelling, blended footer art was built here and then removed, and
     the five slots it needed went with it — size, position, repeat, blend
     and animation. They were general enough to keep and nothing uses them,
     which is the argument for taking them out rather than against it: a
     mechanism with no user is a mechanism nobody can check, and it would
     have sat here defaulting quietly until it broke. The CHANGELOG has what
     it was and why it went; if a theme needs it again, the slots are four
     lines and the reasoning is on record. */
  color: var(--color-text);
  text-align: center;
}

/* `:where()` so this stays a baseline instead of becoming an override.
   As `.site-footer a` it sat at (0,1,1) and outranked every component
   dropped into the footer — the supplier signature came out at full text
   colour no matter what its own rule said. Third time this family of bug
   has appeared here; see BACKLOG. */
.site-footer :where(a) {
  color: inherit;
}

/* Decorative crop of the company name. aria-hidden: it is the same word
   already in the heading, and a screen reader announcing a stray
   fragment would just be noise.

   Faded out downwards rather than cut off. A hard edge across the
   letters reads as a mistake — something clipped by a container that was
   too short — while a dissolve reads as a decision. It also lets the
   mark start stronger at the top than a flat tint dares to, because it
   is on its way out by the time it reaches the text it sits behind.

   `mask-image` degrades to nothing where it is unsupported: the mark
   simply stays flat, which is exactly where it was before. */
.footer-watermark {
  position: absolute;
  inset-inline: 0;
  inset-block-start: var(--space-8);
  font-family: var(--font-heading);
  font-weight: var(--font-weight-heading);
  font-size: clamp(6rem, 20vw, 16rem);
  line-height: 1;
  -webkit-mask-image: linear-gradient(to bottom, #000 20%, transparent 88%);
  mask-image: linear-gradient(to bottom, #000 20%, transparent 88%);
  letter-spacing: var(--tracking-display);
  /* A whisper of the brand over the footer's own surface, not the ramp's
     100. The ramp's light end is a near-white by construction, so on a dark
     theme the quietest thing in the block became the loudest — a cream
     wordmark two hundred pixels tall behind the contact details. Mixed
     against the surface it stays a whisper whichever way the theme goes.
     Same substitution as the empty image frame and the map facade; the
     three of them are one bug wearing three names. */
  /* A slot, because "a whisper of the brand" stops being quiet the moment
     the brand is a bright colour. The mix below is the right default and
     the only one drga ever needs; a theme whose primary is a near-fluorescent
     yellow gets a two-hundred-pixel word in it, which is the loudest thing
     in the block rather than the faintest. Such a theme says so here — and
     what it usually wants is not less brand but *none*: white at a few per
     cent is a watermark, a tinted one is a logo nobody asked to see. */
  color: var(--footer-watermark-color,
    color-mix(in srgb, var(--color-primary) 22%, var(--color-surface)));
  pointer-events: none;
  user-select: none;
}

.footer__inner {
  position: relative;            /* lifts content above the watermark */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
}

.footer-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: var(--radius-lg);
  /* Slots, defaults equal to the old literals: the tile was born as a
     brand-violet badge for an icon drawn in currentColor. A theme whose
     mark is a fixed-colour artwork (vitarent's black house, yellow leaf)
     flips the surface light instead — the artwork cannot adapt to the
     tile, so the tile adapts to the artwork. */
  background: var(--footer-mark-surface, var(--color-primary));
  color: var(--footer-mark-ink, var(--color-text-on-primary));
  box-shadow: var(--shadow-md);
}

.footer__title {
  font-size: var(--text-2xl);
  text-wrap: balance;
}

.footer__meta {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* Contact pills — social profiles when a client has them, phone and
   e-mail when they do not. Same component either way. */
.footer-pills {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  margin-block-start: var(--space-8);
}

.footer-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-2);
  padding-inline-start: var(--space-6);
  border-radius: var(--radius-full);
  background: var(--color-surface);
  box-shadow: var(--shadow-md);
  font-weight: var(--font-weight-bold);
  text-decoration: none;
  transition:
    translate var(--motion-duration-slow) var(--ease-out-expo),
    box-shadow var(--motion-duration-slow) var(--ease-out-expo);
}

.footer-pill:hover {
  translate: 0 -4px;
  box-shadow: var(--shadow-lg);
}

/* Icon only — the mark is the whole control, so the leading text inset
   collapses and the pill becomes a circle. */
.footer-pill--icon {
  padding-inline-start: var(--space-2);
}

/* The icon-only pills wrap as one item, never one at a time.

   Loose in the row, the phone and e-mail pills are wide enough on a
   phone that the two round buttons broke up: Facebook tucked itself onto
   the end of the e-mail line and Instagram was left alone on a line of
   its own, which reads as a layout that ran out of room rather than one
   that was arranged. Grouped, they either fit beside the others or drop
   to their own line together.

   Same gap as the parent so nothing changes on a wide screen, where the
   whole row fits anyway. */
.footer-pills__social {
  display: flex;
  gap: var(--space-4);
}

.footer-pill__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-full);
  background: var(--color-primary);
  color: var(--color-text-on-primary);
}

.footer-pill__icon .icon {
  width: 20px;
  height: 20px;
}

/* Bottom bar — links, copyright, back to top */
/* Stacked and centred on mobile — left-aligning the bar under a centred
   column reads as a different section rather than its footer line.
   Splits into three zones once there is room. */
.footer-bar {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  margin-block-start: var(--space-16);
  padding-block-start: var(--space-6);
  border-block-start: var(--border-width) solid var(--color-border);
  font-size: var(--text-sm);
}

/* Grid, not `space-between`: with three flex children the middle only
   looks centred when the outer two happen to be the same width. Equal
   `1fr` tracks either side put the copyright on the page's centre line
   regardless of how long the nav or the back-to-top label gets. */
@media (min-width: 48rem) {
  .footer-bar {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
  }

  .footer-bar > :first-child {
    justify-self: start;
  }

  .footer-bar > :last-child {
    justify-self: end;
  }
}

.footer-bar ul {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
}

.footer-bar__legal {
  color: var(--color-text-muted);
}

/* Supplier signature — who built the site, not whose site it is.

   Drawn in `currentColor` rather than the mark's own grey: it has to sit
   on whatever surface a client's footer turns out to be, and a fixed
   #9091A0 disappears on a dark one. The mark is a single flat colour to
   begin with, so following the surface costs it nothing. Quiet by
   default and only fully present on hover — it is a credit, not a
   second logo. */
.footer-made-by {
  display: block;
  width: fit-content;
  margin-inline: auto;
  margin-block-start: var(--space-8);
  color: var(--color-text-muted);
  opacity: 0.6;
  transition:
    opacity var(--motion-duration) var(--ease-out-expo),
    color var(--motion-duration) var(--ease-out-expo);
}

.footer-made-by:hover {
  opacity: 1;
  color: var(--color-primary);
}

.footer-made-by svg {
  display: block;
}

.footer-top {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: var(--font-weight-bold);
  text-decoration: none;
}

.footer-top .icon {
  width: 20px;
  height: 20px;
  transition: translate var(--motion-duration-slow) var(--ease-out-expo);
}

.footer-top:hover .icon {
  translate: 0 -4px;
}
