/* ==========================================================================
   Shared base styles
   Version: v1.2.0
   Created: 31 August 2026
   Updated: 31 August 2026

   Deploy path: /shared/css/base.css

   Everything here reads tokens and nothing here hardcodes a brand value.
   This file is identical on every site in the estate; a fix here deploys
   to all of them.

   Contents:
      1. Reset
      2. Document and typography defaults
      3. Links, focus, selection
      4. Accessibility utilities
      5. Layout container
      6. Grid
      7. Visibility
      8. Text block      (component, new in v1.2.0)
      9. Divider         (component)
     10. Button          (component)
     11. Image and media (component, expanded in v1.2.0)
     12. Print

   Not here yet, and coming in the next build: navigation, header, footer,
   forms.

   Change log
     v1.2.0  Added the text block component (section 8) and rebuilt the
             media defaults into a full image component (section 11).
             Added .btn-group to the Button section, because the text
             block's two-CTA pattern needs it. Divider, Button and Print
             renumbered. .text-block joins .flow on the section 2 spacing
             rules so the spacing lives in exactly one place. No token was
             added, removed or renamed; template parity is unaffected.
     v1.1.0  Added the responsive 12-column grid (section 6) and the
             per-breakpoint visibility utility (section 7). Divider,
             Button, Media and Print renumbered from 6-9 to 8-11.
             Added .visually-hidden-focusable alongside .visually-hidden.
             No token was added, removed or renamed — the grid runs on
             --grid-columns, --grid-gap and --grid-gap-row, which already
             existed in the template. Template parity is unaffected.
     v1.0.0  Initial creation.
   ========================================================================== */

/* ==========================================================================
   1. Reset
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body,
h1, h2, h3, h4, h5, h6,
p, figure, blockquote, dl, dd {
  margin: 0;
}

ul[role='list'],
ol[role='list'] {
  list-style: none;
  margin: 0;
  padding: 0;
}

input, button, textarea, select {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: 0;
  cursor: pointer;
}

/* ==========================================================================
   2. Document and typography defaults
   ========================================================================== */

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  min-height: 100vh;
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--step-0);
  font-weight: var(--weight-regular);
  line-height: var(--leading-normal);
  color: var(--color-text);
  background-color: var(--color-surface);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-strong);
  text-wrap: balance;
}

h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }
h5 { font-size: var(--step-0); }
h6 {
  font-size: var(--step--1);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
}

/* Vertical rhythm. Flow spacing rather than per-element margins, so a
   heading that follows a paragraph is spaced without any element knowing
   what comes before it.

   .text-block (section 8) shares these rules rather than restating them.
   If the rhythm ever changes, it changes here and nowhere else. */
.flow > * + *,
.text-block > * + * {
  margin-block-start: var(--space-md);
}

.flow > * + :is(h2, h3, h4),
.text-block > * + :is(h2, h3, h4) {
  margin-block-start: var(--space-xl);
}

p {
  text-wrap: pretty;
}

/* Constrain body copy to a comfortable line length. */
.prose {
  max-width: var(--measure);
}

.prose-narrow {
  max-width: var(--measure-narrow);
}

.lead {
  font-size: var(--step-1);
  color: var(--color-text-muted);
}

.eyebrow {
  font-family: var(--font-body);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-eyebrow);
  text-transform: uppercase;
  color: var(--color-text-muted);
}

small {
  font-size: var(--step--1);
}

strong, b {
  font-weight: var(--weight-semibold);
}

code, kbd, samp, pre {
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* ==========================================================================
   3. Links, focus, selection
   ========================================================================== */

a {
  color: var(--color-link);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}

a:hover {
  color: var(--color-link-hover);
}

a:visited {
  color: var(--color-link-visited);
}

/* One focus style for the whole estate. Never remove an outline without
   putting an equivalent back — the Duda site removed it on the hamburger
   toggle and that is one of the defects being fixed. */
:focus-visible {
  outline: var(--focus-ring-width) var(--focus-ring-style) var(--color-focus-ring);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-xs);
}

::selection {
  background-color: var(--color-selection-bg);
  color: var(--color-selection-text);
}

/* ==========================================================================
   4. Accessibility utilities
   ========================================================================== */

/* Hidden visually, still read by a screen reader. Stays hidden even when
   focused. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Hidden visually until it receives focus, then rendered normally. This is
   deliberately a separate, differently named utility from .visually-hidden
   and from the section 7 breakpoint utilities — the three do different
   things and must not be confused for each other:

     .visually-hidden            off-screen always, always announced
     .visually-hidden-focusable  off-screen until focused, always announced
     .u-hide-*                   display:none, removed from the
                                 accessibility tree entirely

   Use this for anything a keyboard user must be able to reach but a
   sighted user should not see until they tab to it. */
.visually-hidden-focusable:not(:focus):not(:focus-within) {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Skip link. Off-screen until focused, then pinned top-left above
   everything including an open drawer. */
.skip-link {
  position: absolute;
  top: var(--space-2xs);
  left: var(--space-2xs);
  z-index: var(--z-skip-link);
  padding: var(--space-2xs) var(--space-sm);
  font-size: var(--step--1);
  font-weight: var(--weight-semibold);
  color: var(--color-text-inverse);
  background-color: var(--color-brand-800);
  border-radius: var(--radius-sm);
  text-decoration: none;
  transform: translateY(-200%);
  transition: transform var(--duration-fast) var(--ease-out);
}

.skip-link:focus-visible {
  transform: translateY(0);
  color: var(--color-text-inverse);
}

/* Every interactive control clears the minimum touch target. */
a[class*='btn'],
button,
[role='button'],
summary {
  min-height: var(--tap-target-min);
}

/* ==========================================================================
   5. Layout container
   ========================================================================== */

.container {
  width: 100%;
  max-width: calc(var(--width-container) + (var(--space-gutter) * 2));
  margin-inline: auto;
  padding-inline: var(--space-gutter);
}

.container-wide {
  max-width: calc(var(--width-container-wide) + (var(--space-gutter) * 2));
}

.container-narrow {
  max-width: calc(var(--width-container-narrow) + (var(--space-gutter) * 2));
}

/* Section: vertical rhythm between major page blocks. */
.section {
  padding-block: var(--space-section);
}

.section-tight {
  padding-block: var(--space-section-tight);
}

.section-loose {
  padding-block: var(--space-section-loose);
}

/* Full-bleed: background spans the viewport, content stays in the
   container. This is the site's primary section pattern, 28 instances. */
.section-bleed {
  width: 100%;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.section-bleed.has-overlay {
  position: relative;
  isolation: isolate;
}

.section-bleed.has-overlay::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background-color: var(--color-overlay);
}

.section-inverse {
  color: var(--color-text-inverse);
  background-color: var(--color-surface-inverse);
}

.section-inverse :is(h1, h2, h3, h4, h5, h6) {
  color: var(--color-text-inverse);
}

.section-inverse a {
  color: var(--color-link-inverse);
}

.section-sunken {
  background-color: var(--color-surface-sunken);
}

/* Text alignment as a documented row option rather than an inline style. */
.text-start  { text-align: start; }
.text-center { text-align: center; }
.text-end    { text-align: end; }

/* ==========================================================================
   6. Grid
   Replaces Duda's .dmRespRow / .dmRespCol.medium-N.large-N pair.
   133 rows and 217 columns across the pilot's 11 pages; everything else
   on the site nests inside one of them.

   Model
     .grid          the row. A real CSS grid, 12 columns wide.
     .col-N         span N columns at every width.
     .col-md-N      span N columns from 768px up.
     .col-lg-N      span N columns from 1024px up.
     .start-md-N    begin at column N from 768px up.
     .start-lg-N    begin at column N from 1024px up.

   Two things worth knowing before using it:

   1. A child with no .col class is full width. Mobile-first is the
      default, so a plain two-item .grid stacks on a phone and needs only
      .col-md-6 on each child to sit side by side from 768px.

   2. The start-* classes exist so an offset never needs an empty div.
      The Duda site carries 23 .empty-column elements whose only job is to
      push the next column across; those become a start-md-N or start-lg-N
      on the real column and the empty div is dropped.

   Column count and both gaps come from --grid-columns, --grid-gap and
   --grid-gap-row. Changing the grid in one place changes it everywhere.
   ========================================================================== */

.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-columns), minmax(0, 1fr));
  column-gap: var(--grid-gap);
  row-gap: var(--grid-gap-row);
}

/* Default: every child fills the row. Overridden by any .col class below.
   minmax(0, 1fr) above is what stops a long word or a wide image from
   forcing a column past its share of the row. */
.grid > * {
  grid-column: 1 / -1;
  min-width: 0;
}

/* Gap variants. */
.grid-tight {
  column-gap: var(--space-sm);
  row-gap: var(--space-md);
}

.grid-loose {
  column-gap: var(--space-lg);
  row-gap: var(--space-2xl);
}

.grid-flush {
  column-gap: 0;
  row-gap: 0;
}

/* Vertical alignment of columns within a row. Default is stretch, which
   makes equal-height columns; .items-center is the one to reach for on an
   image-beside-text row where the shorter side should sit centred. */
.grid.items-start   { align-items: start; }
.grid.items-center  { align-items: center; }
.grid.items-end     { align-items: end; }
.grid.items-stretch { align-items: stretch; }

/* --- Spans at every width ------------------------------------------------ */

.col-1  { grid-column: auto / span 1; }
.col-2  { grid-column: auto / span 2; }
.col-3  { grid-column: auto / span 3; }
.col-4  { grid-column: auto / span 4; }
.col-5  { grid-column: auto / span 5; }
.col-6  { grid-column: auto / span 6; }
.col-7  { grid-column: auto / span 7; }
.col-8  { grid-column: auto / span 8; }
.col-9  { grid-column: auto / span 9; }
.col-10 { grid-column: auto / span 10; }
.col-11 { grid-column: auto / span 11; }
.col-12 { grid-column: auto / span 12; }

/* --- Spans from 768px up ------------------------------------------------- */

@media (min-width: 768px) {
  .col-md-1  { grid-column: auto / span 1; }
  .col-md-2  { grid-column: auto / span 2; }
  .col-md-3  { grid-column: auto / span 3; }
  .col-md-4  { grid-column: auto / span 4; }
  .col-md-5  { grid-column: auto / span 5; }
  .col-md-6  { grid-column: auto / span 6; }
  .col-md-7  { grid-column: auto / span 7; }
  .col-md-8  { grid-column: auto / span 8; }
  .col-md-9  { grid-column: auto / span 9; }
  .col-md-10 { grid-column: auto / span 10; }
  .col-md-11 { grid-column: auto / span 11; }
  .col-md-12 { grid-column: auto / span 12; }

  .start-md-1    { grid-column-start: 1; }
  .start-md-2    { grid-column-start: 2; }
  .start-md-3    { grid-column-start: 3; }
  .start-md-4    { grid-column-start: 4; }
  .start-md-5    { grid-column-start: 5; }
  .start-md-6    { grid-column-start: 6; }
  .start-md-7    { grid-column-start: 7; }
  .start-md-8    { grid-column-start: 8; }
  .start-md-9    { grid-column-start: 9; }
  .start-md-10   { grid-column-start: 10; }
  .start-md-11   { grid-column-start: 11; }
  .start-md-12   { grid-column-start: 12; }
  .start-md-auto { grid-column-start: auto; }
}

/* --- Spans from 1024px up ------------------------------------------------ */

@media (min-width: 1024px) {
  .col-lg-1  { grid-column: auto / span 1; }
  .col-lg-2  { grid-column: auto / span 2; }
  .col-lg-3  { grid-column: auto / span 3; }
  .col-lg-4  { grid-column: auto / span 4; }
  .col-lg-5  { grid-column: auto / span 5; }
  .col-lg-6  { grid-column: auto / span 6; }
  .col-lg-7  { grid-column: auto / span 7; }
  .col-lg-8  { grid-column: auto / span 8; }
  .col-lg-9  { grid-column: auto / span 9; }
  .col-lg-10 { grid-column: auto / span 10; }
  .col-lg-11 { grid-column: auto / span 11; }
  .col-lg-12 { grid-column: auto / span 12; }

  .start-lg-1    { grid-column-start: 1; }
  .start-lg-2    { grid-column-start: 2; }
  .start-lg-3    { grid-column-start: 3; }
  .start-lg-4    { grid-column-start: 4; }
  .start-lg-5    { grid-column-start: 5; }
  .start-lg-6    { grid-column-start: 6; }
  .start-lg-7    { grid-column-start: 7; }
  .start-lg-8    { grid-column-start: 8; }
  .start-lg-9    { grid-column-start: 9; }
  .start-lg-10   { grid-column-start: 10; }
  .start-lg-11   { grid-column-start: 11; }
  .start-lg-12   { grid-column-start: 12; }
  .start-lg-auto { grid-column-start: auto; }
}

/* ==========================================================================
   7. Visibility
   Replaces Duda's five overlapping runtime classes with four that do not
   overlap. Requirements are from ganngroup-device-diff-v1_0_0.md §8.

   Duda class      Replacement           Behaviour
     hide-for-small  .u-hide-below-768     hidden below 768px
     showOnMedium    .u-hide-from-1024     hidden from 1024px up
     showOnLarge     .u-hide-below-1024    hidden below 1024px
     hide-for-medium .u-hide-768-to-1023   hidden across the tablet band

   These use display:none, which removes the element from the accessibility
   tree as well as from view. That is the intended behaviour: hidden means
   hidden for everyone. If something must stay reachable by keyboard while
   invisible, it wants .visually-hidden-focusable in section 4 instead.

   No !important anywhere. Duda needs it because it is fighting its own
   cascade; nothing in this kit competes with these rules.

   Two notes on scope:

   * Image switching by breakpoint is not a visibility problem and gets no
     utility here. Every image uses srcset and sizes, and the browser picks
     the variant. Never ship the same image twice and hide one copy.

   * The nav is not built by hiding one menu and showing another. One nav
     ships in the markup and is restyled across the breakpoints. Duda ships
     the full menu twice, which is most of why its desktop document is 12%
     larger, and it puts two DOM nodes in the same landmark role.
     .u-hide-from-1024 and .u-hide-below-1024 are for the header bar, the
     hamburger button and the sticky-header spacer — chrome around the nav,
     not the nav itself.

   All four are scoped to `screen` on purpose. A print stylesheet is
   evaluated against the paper width, which lands somewhere arbitrary
   inside these bands and would otherwise drop real content from a printed
   page. Section 11 hides the chrome that genuinely should not print.

   Each selector is written twice on purpose — .u-hide-x.u-hide-x. That is
   not a typo. A single class scores the same specificity as a component
   rule such as `.site-header-bar { display: flex }`, and a site.css that
   loads after this file would then win the tie on source order and the
   element would stay visible with no error anywhere. Doubling the class
   makes the utility out-specify any single-class component rule while
   still losing to an inline style, which is the behaviour a utility should
   have. This is what replaces the !important Duda relies on.
   ========================================================================== */

@media screen and (max-width: 767.98px) {
  .u-hide-below-768.u-hide-below-768 { display: none; }
}

@media screen and (min-width: 1024px) {
  .u-hide-from-1024.u-hide-from-1024 { display: none; }
}

@media screen and (max-width: 1023.98px) {
  .u-hide-below-1024.u-hide-below-1024 { display: none; }
}

@media screen and (min-width: 768px) and (max-width: 1023.98px) {
  .u-hide-768-to-1023.u-hide-768-to-1023 { display: none; }
}

/* ==========================================================================
   8. Text block
   The most-used component on the estate. A heading, some prose, sometimes
   an eyebrow above and a call to action below, held to a readable line
   length.

   Markup contract — everything is optional except the block itself:

     <div class="text-block">
       <p class="eyebrow">Selling</p>
       <h2>What we do</h2>
       <p class="lead">One-sentence summary.</p>
       <p>Body copy.</p>
       <div class="btn-group">
         <a class="btn btn-primary" href="/contact">Get in touch</a>
       </div>
     </div>

   Vertical spacing comes from the .flow rules in section 2. Nothing in
   this section restates them, so a change to the rhythm cannot leave the
   two out of step.

   Width is the point of the component. Prose set the full width of a
   1120px container runs past 120 characters a line, which is roughly
   twice a comfortable measure and the single most common readability
   fault on the current site. .text-block caps at --measure by default.
   ========================================================================== */

.text-block {
  max-width: var(--measure);
}

/* Tighter measure, for a short intro or a column inside a grid. */
.text-block-narrow {
  max-width: var(--measure-narrow);
}

/* No cap. For a block that is already inside a narrow grid column, where
   a second limit would leave the column half empty. */
.text-block-wide {
  max-width: none;
}

/* Centred text AND a centred block. Setting text-align alone leaves the
   measure pinned to the left edge of its container, which reads as a
   mistake at wide viewports. */
.text-block-center {
  margin-inline: auto;
  text-align: center;
}

/* A centred block whose text stays left-aligned. */
.text-block-middle {
  margin-inline: auto;
}

/* Lists inside prose. [role='list'] is excluded because that marks a list
   deliberately stripped of its bullets in section 1 — usually a nav or a
   card set — and it should not get them back here. */
.text-block :is(ul, ol):not([role='list']) {
  padding-inline-start: var(--space-md);
}

.text-block :is(ul, ol):not([role='list']) li + li {
  margin-block-start: var(--space-2xs);
}

.text-block li::marker {
  color: var(--color-accent-700);
}

/* A centred block centres its list too, which needs the default inline
   padding removed or the list sits visibly off-centre. */
.text-block-center :is(ul, ol):not([role='list']) {
  padding-inline-start: 0;
  list-style-position: inside;
}

/* Quoted copy. Not a testimonial component — that comes later with its
   own attribution and layout — just a pull quote inside running text. */
.text-block blockquote {
  padding-inline-start: var(--space-md);
  border-inline-start: var(--border-width-heavy) solid var(--color-accent-600);
  font-size: var(--step-1);
  color: var(--color-text-muted);
}

/* On an inverse background the muted greys and the bronze marker both
   drop below contrast. Restate them against the dark surface. */
.section-inverse .text-block :is(.lead, .eyebrow, blockquote) {
  color: var(--color-brand-200);
}

.section-inverse .text-block li::marker {
  color: var(--color-accent-400);
}

/* ==========================================================================
   9. Divider
   45 instances across the pilot. A real <hr>, which carries the correct
   separator semantics without any ARIA.
   ========================================================================== */

hr,
.divider {
  height: 0;
  border: 0;
  border-block-start: var(--divider-width) solid var(--divider-color);
  margin-block: var(--divider-margin);
}

.divider-tight {
  margin-block: var(--space-sm);
}

.divider-accent {
  border-block-start-color: var(--color-accent-600);
}

/* Short centred rule, for use under a section heading. */
.divider-short {
  width: 4rem;
  margin-inline: auto;
  border-block-start-width: var(--border-width-heavy);
  border-block-start-color: var(--color-accent-600);
}

/* ==========================================================================
   10. Button
   Pure CSS, variants by class. An <a> when it navigates, a <button> when
   it acts — both render identically.
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  min-height: var(--btn-min-height);
  padding: var(--btn-padding-block) var(--btn-padding-inline);
  font-family: var(--btn-font);
  font-size: var(--btn-font-size);
  font-weight: var(--btn-font-weight);
  line-height: var(--leading-none);
  letter-spacing: var(--btn-tracking);
  text-align: center;
  text-decoration: none;
  text-transform: uppercase;
  border: var(--btn-border-width) solid transparent;
  border-radius: var(--btn-radius);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out),
              color var(--duration-fast) var(--ease-out);
}

/* Full width on a phone, fixed width from 768px up. This is the estate's
   established behaviour and the 280px is carried over deliberately. */
.btn {
  width: 100%;
}

@media (min-width: 768px) {
  .btn {
    width: auto;
    min-width: var(--btn-min-width);
  }

  .btn-auto {
    min-width: 0;
  }
}

.btn-primary {
  color: var(--btn-primary-text);
  background-color: var(--btn-primary-bg);
  border-color: var(--btn-primary-border);
}

.btn-primary:hover,
.btn-primary:visited:hover {
  color: var(--btn-primary-text);
  background-color: var(--btn-primary-bg-hover);
}

.btn-primary:visited {
  color: var(--btn-primary-text);
}

.btn-secondary {
  color: var(--btn-secondary-text);
  background-color: var(--btn-secondary-bg);
  border-color: var(--btn-secondary-border);
}

.btn-secondary:hover,
.btn-secondary:visited:hover {
  color: var(--btn-secondary-text);
  background-color: var(--btn-secondary-bg-hover);
}

.btn-secondary:visited {
  color: var(--btn-secondary-text);
}

.btn-ghost {
  color: var(--btn-ghost-text);
  background-color: var(--btn-ghost-bg);
  border-color: var(--btn-ghost-border);
}

.btn-ghost:hover,
.btn-ghost:visited:hover {
  color: var(--btn-ghost-text);
  background-color: var(--btn-ghost-bg-hover);
}

.btn-ghost:visited {
  color: var(--btn-ghost-text);
}

.btn[aria-disabled='true'],
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

/* Two or more buttons together. The homepage carries a pair. Buttons are
   full width below 768px, so the group stacks there and only becomes a
   row once the buttons have stopped filling the line themselves. */
.btn-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

@media (min-width: 768px) {
  .btn-group {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }

  .text-block-center .btn-group {
    justify-content: center;
  }
}

/* ==========================================================================
   11. Image and media
   Every image on the estate is one <img> with srcset and sizes. No JS
   switching, no device sniffing, and never the same picture shipped twice
   with one copy hidden.

   Markup contract:

     <img src="hero-1024.jpg"
          srcset="hero-480.jpg 480w, hero-768.jpg 768w,
                  hero-1024.jpg 1024w, hero-1280.jpg 1280w,
                  hero-1920.jpg 1920w"
          sizes="(min-width: 1200px) 1120px, 100vw"
          width="1920" height="1080"
          alt="Describes the picture, or alt='' if it is decoration"
          loading="lazy" decoding="async">

   Five rules that are not negotiable, each of them fixing something the
   current site gets wrong:

   1. width and height are always present and always the real pixel
      dimensions of the src file. The browser reserves the right box
      before the bytes arrive. Duda emits width="0.0" and every page
      shifts as it loads.

   2. The first image in the viewport — the hero — takes
      loading="eager" and fetchpriority="high". Everything below the fold
      takes loading="lazy". Lazy-loading the hero delays the largest
      element on the page and is a measurable regression, not a saving.

   3. alt is a decision, never an omission. Decorative images take alt=""
      so a screen reader skips them. Not one image on the current site
      has any alt text at all, so every one of them is a decision still
      to be made.

   4. Ladder widths are 480 / 768 / 1024 / 1280 / 1920, matching the
      breakpoint scale, with 2560 added only for a full-bleed background.
      Duda ships two rungs — 640 and 1920 — so a tablet downloads the
      desktop asset.

   5. Never generate a variant wider than the source. Several -1920w
      files on the current site are byte-identical to their -640w
      siblings because the upload was smaller than either, so mobile
      downloads a desktop-sized file for no benefit.

   sizes for the common grid cases, given a 1120px container:

     full width of the container   (min-width: 1200px) 1120px, 100vw
     half at md, half at lg        (min-width: 1200px) 544px,
                                   (min-width: 768px) 50vw, 100vw
     third at lg, half at md       (min-width: 1200px) 352px,
                                   (min-width: 768px) 50vw, 100vw

   Art direction — a different crop rather than a different size — is the
   one case for <picture>, and it is the only place a media attribute
   belongs:

     <picture>
       <source media="(max-width: 767px)" srcset="hero-portrait.jpg">
       <img src="hero-landscape.jpg" width="1920" height="1080" alt="…">
     </picture>
   ========================================================================== */

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

/* There is deliberately no `img[width][height]` rule here. An earlier
   draft carried one setting height:auto, which was redundant — the rule
   above already does it — and actively harmful, because an attribute
   selector out-specifies `.media > img` and stopped every cropped image
   from filling its box. Space is reserved by the width and height
   attributes in the markup, which the browser turns into an aspect ratio
   on its own. Nothing in CSS is needed for it. */

figure {
  margin: 0;
}

figcaption {
  margin-block-start: var(--space-2xs);
  font-size: var(--step--1);
  color: var(--color-text-muted);
}

.section-inverse figcaption {
  color: var(--color-brand-200);
}

/* --- Cropped media box ---------------------------------------------------
   Wrap an image in .media when the layout needs a fixed shape and the
   source photos do not agree on one — a row of three panels where one
   picture is portrait, or an agent headshot set. The box holds the ratio
   and the image fills it.

   Without a ratio class the box takes its shape from the image, which is
   the right default; add one only when the shape has to be forced.
   ------------------------------------------------------------------------- */

.media {
  position: relative;
  overflow: hidden;
  background-color: var(--media-placeholder);
  border-radius: var(--media-radius);
}

.media > :is(img, picture, video, svg) {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

.media-16x9 { aspect-ratio: 16 / 9; }
.media-3x2  { aspect-ratio: 3 / 2; }
.media-4x3  { aspect-ratio: 4 / 3; }
.media-1x1  { aspect-ratio: 1 / 1; }
.media-3x4  { aspect-ratio: 3 / 4; }

/* Fill the height of a grid column instead of a fixed ratio. Use it on
   an image sitting beside a text block of unknown length. */
.media-fill {
  height: 100%;
}

/* Keep the whole subject in frame and letterbox the remainder. For logos
   and anything where a crop would cut something important. */
.media-contain > :is(img, picture, video, svg) {
  object-fit: contain;
}

/* Move the crop off centre. A headshot cropped to a square loses the top
   of the head at the default centre position. */
.media-top    > :is(img, picture, video, svg) { object-position: top; }
.media-bottom > :is(img, picture, video, svg) { object-position: bottom; }

/* --- Standalone image modifiers -----------------------------------------
   For an image that needs no crop box. These apply to the <img> itself.
   ------------------------------------------------------------------------- */

.img-rounded { border-radius: var(--media-radius); }
.img-round   { border-radius: var(--radius-lg); }
.img-circle  { border-radius: var(--radius-full); }

/* Centre an image narrower than its column. */
.img-center { margin-inline: auto; }

/* Cap an image that would otherwise dominate the column, and keep it
   from being upscaled past its own resolution. */
.img-constrain {
  width: 100%;
  max-width: 32rem;
}

.section-inverse .media {
  background-color: var(--color-brand-800);
}

/* --- Embed ---------------------------------------------------------------
   Responsive embed, for the four YouTube videos on /first. Ratio comes
   from --video-aspect so a site with 4:3 archive footage can change it in
   one token rather than in the markup.
   ------------------------------------------------------------------------- */

.embed {
  aspect-ratio: var(--video-aspect);
  width: 100%;
  background-color: var(--media-placeholder);
  border-radius: var(--media-radius);
  overflow: hidden;
}

.embed > iframe {
  width: 100%;
  height: 100%;
  border: 0;
}

/* ==========================================================================
   12. Print
   ========================================================================== */

@media print {
  .skip-link,
  .site-header,
  .site-footer {
    display: none;
  }

  body {
    color: #000;
    background: #fff;
  }

  a[href^='http']::after {
    content: ' (' attr(href) ')';
    font-size: 0.85em;
    word-break: break-all;
  }
}
