@charset "utf-8";

/* ==========================================================================
   Typefaces

   Beausite Classic and Rhymes Text, as specified in the brand document.

   BEAUSITE IS LICENSED. These are Fatype's own WOFF2 files, copied in byte for
   byte: the licence forbids modification, so they are NOT subset, which is why
   Clear is 73KB rather than the 9KB the trial cut was. The full 1030-glyph set
   is the point — the trial had 155 and no ampersand, so every "Storage &
   Handling" on the site was quietly falling back to Helvetica for that one
   character.

   Two conditions come with it, and neither is satisfied yet:
     - the site has to be registered at fatype.com/user/licenses, per domain;
     - Fatype require their htaccess referrer-guard beside the font files, and
       say in the policy that failing to install it terminates the agreement.
       GitHub Pages cannot run htaccess at all. See README, "Fonts".

   RHYMES IS LICENSED TOO, and shipped as WOFF rather than WOFF2. That is on
   purpose: MaxiType's licence excludes "modifying, reassembling", and
   recompressing a WOFF into a WOFF2 is arguably both, so these are their files
   exactly as delivered. It costs about 17KB a face over WOFF2 and WOFF is
   supported everywhere, so nothing is lost but bytes.

   The order has Light (300) and Medium (500) and no 400, which is what the
   body copy used to be set in. Body copy is now Light: almost all of it is
   white on the ultramarine, and light type on a dark ground blooms optically —
   it prints heavier than the same weight would on paper. Medium reads chunky
   against the hairline wordmark. One number changes it back, on body below.
   ========================================================================== */

@font-face {
  font-family: "Beausite Classic";
  src: url("../fonts/BeausiteClassicWeb-Clear.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
/* Bought alongside Clear and declared so the family is complete. Nothing on the
   site asks for it yet — there is no bold anywhere in the markup. */
@font-face {
  font-family: "Beausite Classic";
  src: url("../fonts/BeausiteClassicWeb-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rhymes Text";
  src: url("../fonts/RhymesText-Light.woff") format("woff");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Rhymes Text";
  src: url("../fonts/RhymesText-Medium.woff") format("woff");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("../fonts/ibm-plex-mono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
/* ==========================================================================
   Storehouse Fine Arts
   Built from the Storehouse Brand Identity (2025) by Dougal.

   Principles carried into code:
   Clarity    — nothing on the page that is not doing work.
   Structure  — one grid, one scale, one rule weight.
   Stillness  — motion is slow, optional, and never decorative.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */

:root {
  /* Palette — taken directly from the brand document, page 10. */
  --ultramarine: #0f1b70;
  --charcoal:    #1f1f1f;
  --slate:       #9a9a9a;
  --canvas:      #fdfdfd;
  --paper:       #f4f4f4;

  /* The page sits on the brand blue, and the ink is the paper colour. Every
     rule below is written against --ground / --ink rather than against a fixed
     colour, so a section can invert simply by redeclaring these six lines.
     .on-paper does exactly that. */
  --ground:      var(--ultramarine);
  --ink:         var(--canvas);
  --ink-muted:        color-mix(in srgb, var(--canvas) 62%, var(--ultramarine));
  --ink-strong-muted: color-mix(in srgb, var(--canvas) 84%, var(--ultramarine));
  --accent:      var(--canvas);          /* the hover / active colour */
  --rule:        rgba(253, 253, 253, 0.2);
  --rule-strong: rgba(253, 253, 253, 0.48);
  --plate-bg:    color-mix(in srgb, var(--canvas) 10%, var(--ultramarine));

  /* Typography — the brand faces, with a close fallback while they load and
     for the handful of characters the trial cut is missing. */
  --sans:  "Beausite Classic", "Instrument Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --serif: "Rhymes Text", "Newsreader", "Iowan Old Style", "Times New Roman", Georgia, serif;
  --mono:  "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* A single modular scale, fluid between 380px and 1600px. */
  --t-display: clamp(2.6rem, 1.4rem + 5.2vw, 6rem);
  --t-title:   clamp(1.75rem, 1.2rem + 2.4vw, 3.1rem);
  --t-lead:    clamp(1.15rem, 1.02rem + 0.55vw, 1.5rem);
  --t-body:    clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
  --t-small:   0.875rem;
  --t-label:   0.6875rem;

  /* Spacing — an 8px base, named by role rather than size. */
  --gutter:  clamp(1.25rem, 0.6rem + 2.6vw, 3.5rem);
  --measure: 34em;
  --band:    clamp(3.25rem, 1.6rem + 4.6vw, 6rem);   /* between major sections */
  --stack:   clamp(1.5rem, 0.9rem + 1.5vw, 2.25rem); /* within a section       */

  /* Set from the real masthead height on load, so the first screen ends
     exactly at the fold on any viewport. */
  --masthead-h: 5.5rem;
  --footer-h: 16rem;

  --shell: 88rem;  /* maximum line of the page */

  color-scheme: dark;
}

/* The inverse ground: paper, with the blue as ink. Used by the panels, the
   footer, the mobile menu and the map dialog — anything that was blue before
   the palette was turned over. */
.on-paper, .panel, .footer, .menu, .map-dialog {
  --ground:      var(--canvas);
  --ink:         var(--charcoal);
  --ink-muted:        #6b6b6b;
  /* Charcoal lifted toward slate: darker than --ink-muted, same family. */
  --ink-strong-muted: #4a4a4a;
  --accent:      var(--ultramarine);
  --rule:        rgba(31, 31, 31, 0.16);
  --rule-strong: rgba(31, 31, 31, 0.4);
  --plate-bg:    var(--paper);
  background: var(--ground);
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   2. Reset and base
   -------------------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--masthead-h) + var(--filter-h, 0px));
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--serif);
  font-size: var(--t-body);
  /* Rhymes Light. This is the serif's weight, and the one number to change if
     the body copy should be Medium instead.

     Everything set in the sans or the mono inherits this 300 and never gets a
     light face, because neither family has one: the font-matching rules say a
     request below 400 with nothing at or under it takes the next weight up,
     which is Beausite Clear and Plex Mono at 400. Verified in the browser
     rather than assumed. */
  font-weight: 300;
  line-height: 1.62;
  font-optical-sizing: auto;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-family: var(--sans);
  font-weight: 400;
  letter-spacing: -0.021em;
  line-height: 1.06;
  margin: 0;
  text-wrap: balance;
}

p { margin: 0 0 1.1em; max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

a { color: inherit; }

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

ul, ol { margin: 0; padding: 0; list-style: none; }

::selection { background: var(--ink); color: var(--ground); }

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
  border-radius: 1px;
}

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

.skip-link {
  position: absolute; top: 0; left: 0; z-index: 100;
  background: var(--ink); color: var(--ground);
  font: 400 var(--t-small)/1 var(--sans);
  text-decoration: none;
  /* Clipped where it stands rather than moved above the page. Safari paints
     the area above the document origin during a rubber-band scroll, so a link
     parked up there appeared every time anyone over-scrolled at the top. */
  width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
.skip-link:focus {
  width: auto; height: auto;
  padding: 0.75rem 1.25rem;
  overflow: visible; clip-path: none;
}

/* --------------------------------------------------------------------------
   3. Layout primitives
   -------------------------------------------------------------------------- */

.shell {
  width: min(100% - (var(--gutter) * 2), var(--shell));
  margin-inline: auto;
}

/* The marginalia grid: a short label in the left margin, content on the right.
   This is the layout system from the brand document, page 11. */
.marginalia {
  display: grid;
  gap: 0.75rem var(--gutter);
  grid-template-columns: 1fr;
}

@media (min-width: 56rem) {
  .marginalia {
    grid-template-columns: minmax(9rem, 1fr) minmax(0, 3fr);
    gap: 0 var(--gutter);
  }
}

.marginalia > .aside { grid-column: 1; }
.marginalia > .main  { grid-column: 1; }

@media (min-width: 56rem) {
  .marginalia > .main { grid-column: 2; }
}

.band { padding-block: var(--band); }
/* A band holding nothing but its own heading: what follows brings its own
   top padding, so this one does not need a bottom. */
.band--head { padding-bottom: 0; }
.band + .band { padding-top: 0; }

/* A hairline above an entry, drawn inside the shell so it aligns with the text. */

/* --------------------------------------------------------------------------
   4. The ledger — index numbers, labels and captions
   -------------------------------------------------------------------------- */

.label,
.ledger {
  font-family: var(--mono);
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-muted);
  line-height: 1.5;
}

.ledger { color: var(--ink-muted); font-variant-numeric: tabular-nums; }


/* The title in the margin — the brand document's own page structure. */
.marginalia > .aside .ledger { display: block; margin-bottom: 0.6rem; }
.marginalia > .aside h2 {
  font-size: var(--t-body);
  letter-spacing: -0.016em;
  margin: 0 0 1.25rem;
}
@media (min-width: 56rem) {
  .marginalia > .aside h2 { margin-bottom: 0; }
}

.section-head {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding-block: 1.1rem;
  border-top: 1px solid var(--rule);
  margin-bottom: var(--stack);
}
.section-head h2 {
  font-family: var(--mono);
  font-size: var(--t-label);
  font-weight: 400;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

figcaption {
  margin-top: 0.85rem;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  line-height: 1.6;
  max-width: 30em;
}

/* --------------------------------------------------------------------------
   5. Masthead
   -------------------------------------------------------------------------- */

.masthead {
  position: sticky;
  top: 0;
  z-index: 40;
  /* Nothing at all at the top of a page: an 88% ultramarine bar plus a
     saturating backdrop reads as a slightly different blue against the plain
     ultramarine behind it, which is exactly what you do not want the first
     thing on the page to be. The border is declared transparent rather than
     added later so the 1px is always in the box and the masthead never
     changes height. */
  background: transparent;
  /* Two things keep the bar from flashing as the page starts to move.
     
     It used to be saturate(150%) blur(14px), and backdrop-filter was not in the
     transition list, so the filter landed whole on the first frame while the
     88% ultramarine was still fading in behind it: for a moment you saw the
     saturation boost with nothing damping it, and the bar went bright blue
     before settling. Measured, it jumped from rgb(15,27,112) to rgb(13,26,122)
     and eased back to rgb(14,27,117).
     
     So the filter is in the transition now, and it starts from an identity
     blur(0px) so there is something for the real one to grow out of. And the
     saturation is gone: over a flat ground it did nothing but brighten, which
     is what produced the overshoot, while the blur is what actually frosts the
     photographs that pass underneath. The bar now holds rgb(15,27,112) the
     whole way through — no jump, no hump. */
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  border-bottom: 1px solid transparent;
  transition: background-color 0.4s ease, border-color 0.4s ease,
              backdrop-filter 0.4s ease, -webkit-backdrop-filter 0.4s ease;
}

/* Once the page is moving, the bar separates itself from what is passing
   underneath: the backdrop comes on and the hairline is drawn. */
.masthead[data-scrolled="true"] {
  background: color-mix(in srgb, var(--ultramarine) 88%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom-color: var(--rule);
}

/* Where the masthead is sitting over one of the paper sections it turns over
   with it, so the wordmark never disappears into the ground behind it. */
.masthead[data-over="paper"] {
  background: color-mix(in srgb, var(--canvas) 88%, transparent);
  color: var(--charcoal);
}
.masthead[data-over="paper"] .wordmark,
.masthead[data-over="paper"] .nav a,
.masthead[data-over="paper"] .menu-toggle { color: var(--charcoal); }
.masthead[data-over="paper"] .nav__n { color: #6b6b6b; }
.masthead[data-over="paper"] .nav a[aria-current="page"],
.masthead[data-over="paper"] .nav a[aria-current="page"] .nav__n { color: var(--ultramarine); }
.masthead[data-over="paper"] .nav a[aria-current="page"] .nav__t { border-bottom-color: var(--ultramarine); }
.masthead[data-over="paper"] .nav a:hover .nav__t { border-bottom-color: rgba(31, 31, 31, 0.4); }

/* The same again over the slate, with the backdrop taken from that ground. */
.masthead[data-over="charcoal"] {
  background: color-mix(in srgb, var(--charcoal) 92%, transparent);
  color: var(--canvas);
}
.masthead[data-over="charcoal"] .wordmark,
.masthead[data-over="charcoal"] .nav a,
.masthead[data-over="charcoal"] .menu-toggle { color: var(--canvas); }
.masthead[data-over="charcoal"] .nav__n { color: rgba(253, 253, 253, 0.6); }
.masthead[data-over="charcoal"] .nav a[aria-current="page"],
.masthead[data-over="charcoal"] .nav a[aria-current="page"] .nav__n { color: var(--canvas); }
.masthead[data-over="charcoal"] .nav a[aria-current="page"] .nav__t { border-bottom-color: var(--canvas); }
.masthead[data-over="charcoal"] .nav a:hover .nav__t { border-bottom-color: rgba(253, 253, 253, 0.5); }

.masthead[data-over="slate"] {
  background: color-mix(in srgb, var(--slate) 92%, transparent);
  color: var(--canvas);
}
.masthead[data-over="slate"] .wordmark,
.masthead[data-over="slate"] .nav a,
.masthead[data-over="slate"] .menu-toggle { color: var(--canvas); }
.masthead[data-over="slate"] .nav__n { color: rgba(253, 253, 253, 0.8); }
.masthead[data-over="slate"] .nav a[aria-current="page"],
.masthead[data-over="slate"] .nav a[aria-current="page"] .nav__n { color: var(--canvas); }
.masthead[data-over="slate"] .nav a[aria-current="page"] .nav__t { border-bottom-color: var(--canvas); }
.masthead[data-over="slate"] .nav a:hover .nav__t { border-bottom-color: rgba(253, 253, 253, 0.6); }

.masthead__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gutter);
  padding-block: 1.4rem;
}

.wordmark { display: block; width: clamp(8.5rem, 14vw, 12.5rem); color: var(--ink); }
/* The symbol carries no fill of its own, so every instance takes the ink of
   whatever it sits on. This is what lets the masthead invert over dark sections. */
.wordmark svg, .hero__mark svg {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 702 / 45.31;   /* Safari will not infer this from the viewBox alone */
  overflow: visible;
  fill: currentColor;
}
a.wordmark-link { text-decoration: none; }

.nav { display: none; }

@media (min-width: 56rem) {
  .nav { display: flex; align-items: baseline; gap: clamp(1.25rem, 2.4vw, 2.5rem); }
}

.nav a { display: inline-flex; align-items: flex-start; gap: 0.4rem; }
.nav__n {
  font-family: var(--mono);
  font-size: 0.5938rem;
  line-height: 1.32;          /* sets the number against the top of the label */
  letter-spacing: 0.08em;
  color: var(--ink-muted);
  transition: color 0.25s ease;
}
.nav a[aria-current="page"] .nav__n { color: var(--accent); }

.nav a {
  font-family: var(--sans);
  font-size: var(--t-small);
  letter-spacing: -0.006em;
  text-decoration: none;
  color: var(--ink);
  transition: color 0.25s ease;
}
.nav__t {
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: border-color 0.25s ease;
}
.nav a:hover .nav__t { border-bottom-color: var(--rule-strong); }
.nav a[aria-current="page"] { color: var(--accent); }
.nav a[aria-current="page"] .nav__t { border-bottom-color: var(--accent); }

/* Mobile menu */
.menu-toggle {
  font-family: var(--sans);
  font-size: var(--t-small);
  background: none; border: 0; padding: 0.25rem 0;
  color: var(--ink); cursor: pointer;
}
@media (min-width: 56rem) { .menu-toggle { display: none; } }

.menu {
  position: fixed; inset: 0; z-index: 50;
  display: flex; flex-direction: column;
  padding: 1.4rem var(--gutter) var(--band);
  transform: translateY(-100%);
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
  overflow-y: auto;
}
.menu[data-open="true"] { transform: translateY(0); }
.menu__top { display: flex; justify-content: space-between; align-items: center; }
.menu__top .menu-toggle { display: block; }
.menu__links { margin-top: auto; padding-top: var(--band); display: grid; gap: 0.35rem; }
.menu__links a {
  font-family: var(--sans);
  font-size: clamp(2rem, 9vw, 3.25rem);
  letter-spacing: -0.03em;
  line-height: 1.06;
  text-decoration: none;
  padding-block: 0.18em;
}
.menu__meta { margin-top: var(--stack); color: var(--ink-muted); }
.menu__meta a { color: inherit; }

/* --------------------------------------------------------------------------
   6. Hero
   -------------------------------------------------------------------------- */

.hero {
  padding-top: clamp(2.5rem, 6vw, 5.5rem);
  padding-bottom: clamp(0.5rem, 1.2vh, 1rem);
  min-height: calc(100vh - var(--masthead-h));
  min-height: calc(100svh - var(--masthead-h));
  display: flex;
  flex-direction: column;
  transition: background-color 0.7s cubic-bezier(0.22, 1, 0.36, 1),
              color 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}

/* The wordmark sits low on the screen rather than on its centre line, the way a
   title sits low on an exhibition poster. One auto margin, above it, so the free
   space is all spent there and the cue follows underneath. */
.hero__centre { margin-top: auto; margin-bottom: clamp(0.6rem, 1.6vh, 1.25rem); }

/* Drag across the first screen and it takes the brand colour under the
   pointer — a mark the width of the cursor, laid over everything rather than
   behind it, so it can cross type and image alike. */
.hero-field { position: relative; isolation: isolate; }
html[data-cursor="on"] .hero-field { cursor: none; }

.paint {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  /* A canvas is a replaced element: without an explicit CSS size its layout
     width comes from the backing-store attributes, which are multiplied by the
     device pixel ratio. On a retina screen that is twice the width of the page. */
  width: 100%;
  height: 100%;
}

@media (prefers-reduced-motion: reduce) {
  .paint span { transition: none; }
}

/* Out to the right margin of the page, not to a measure of its own. */
.hero__mark { width: 100%; color: var(--ink); }

.hero__disciplines {
  margin-top: clamp(1.25rem, 3vw, 2.25rem);
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.6rem;
  font-family: var(--sans);
  font-size: clamp(0.8rem, 0.68rem + 0.5vw, 1rem);
  letter-spacing: 0.02em;
  color: var(--ink);
}
.hero__disciplines li { display: flex; align-items: center; gap: 1.6rem; }

/* One after another, wiped in from the left with a little lift under them.
   The whole thing lives inside the motion guard, so the resting state — the
   one anybody who has asked for less movement sees — is simply visible. */
@media (prefers-reduced-motion: no-preference) {
  .hero__disciplines li {
    animation: discipline-in 1.15s cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  .hero__disciplines li:nth-child(1) { animation-delay: 0.30s; }
  .hero__disciplines li:nth-child(2) { animation-delay: 0.43s; }
  .hero__disciplines li:nth-child(3) { animation-delay: 0.56s; }
  .hero__disciplines li:nth-child(4) { animation-delay: 0.69s; }

  @keyframes discipline-in {
    from {
      opacity: 0;
      transform: translateY(0.55rem);
      clip-path: inset(0 100% -20% 0);
    }
    to {
      opacity: 1;
      transform: none;
      clip-path: inset(0 0 -20% 0);
    }
  }
}
.hero__disciplines li:not(:last-child)::after {
  content: "";
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--ink-muted);
  flex: none;
}

/* Stacked on a phone, so no separator is ever left dangling at a line break. */
@media (max-width: 40rem) {
  .hero__disciplines { flex-direction: column; gap: 0.1rem; }
  .hero__disciplines li::after { display: none; }
}



/* A quiet indication that the page continues. */
.scroll-cue {
  display: grid;
  place-items: center;
  align-self: center;
  width: 3rem; height: 3rem;
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink-muted);
  cursor: pointer;
  transition: color 0.3s ease, opacity 0.5s ease;
}
.scroll-cue:hover { color: var(--ink); }
.scroll-cue .chev {
  width: 11px; height: 11px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: rotate(45deg);
}
@media (prefers-reduced-motion: no-preference) {
  .scroll-cue { animation: cue-in 0.9s ease 2.2s backwards; }
  @keyframes cue-in {
    from { opacity: 0; transform: translateY(-0.4rem); }
    to   { opacity: 1; transform: none; }
  }

  .scroll-cue .chev { animation: cue 2.6s ease-in-out infinite; }
  @keyframes cue {
    0%, 60%, 100% { transform: rotate(45deg) translate(0, 0); }
    30%           { transform: rotate(45deg) translate(3px, 3px); }
  }
}

/* --------------------------------------------------------------------------
   7. Page titles (inner pages)
   -------------------------------------------------------------------------- */

.page-head { padding-top: clamp(2.5rem, 6vw, 5.5rem); padding-bottom: var(--band); }
.page-head + .band { padding-top: 0; }
.page-head h1 { font-size: clamp(2.4rem, 1.1rem + 4.2vw, 4.6rem); }
.page-head .ledger { margin-bottom: 1.4rem; display: block; }
.page-head .lead {
  margin-top: clamp(1.75rem, 4vw, 3rem);
  font-size: var(--t-lead);
  line-height: 1.5;
  max-width: 36em;
}

/* --------------------------------------------------------------------------
   8. The Journey — one work, end to end, on a single line
   -------------------------------------------------------------------------- */

.journey {
  padding-block: clamp(2rem, 3.5vw, 3.25rem);
}

/* The row runs the full width of the window, not the text measure. */
/* The strip runs the full width of the window and insets its own content, so
   there are no negative margins to escape the page. */
/* The row sits inside the page rhythm, not as a full-bleed band of its own. */
/* The captions used to sit hard against the section below, so the colour
   changed a line under the last word. */
.journey { padding-block: var(--band); }
.journey .section-head { margin-bottom: 1.25rem; }
.journey__track { width: 100%; margin-top: var(--stack); }

/* One card: the photograph, its number, and what is happening in it. */
.journey__plate { position: relative; }
.journey__plate img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  background: var(--plate-bg);
}
.journey__n {
  display: block;
  margin-top: 0.7rem;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.13em;
  color: var(--ink-muted);
}
.journey__stage figcaption {
  margin-top: 0.15rem;
  font-family: var(--sans);
  font-size: var(--t-small);
  letter-spacing: -0.01em;
}

/* One line of cards that drifts along on its own, and can be dragged. */
.journey__grid { position: relative; }

.journey__stages {
  --jgap: clamp(0.75rem, 1.3vw, 1.4rem);
  display: flex;
  gap: var(--jgap);
  overflow-x: auto;
  scrollbar-width: none;
  /* No scroll-snap and no -webkit-overflow-scrolling. Both fight a flick on
     iOS, and with scroll-padding set the last card can never reach a valid
     snap position, so the strip kept dragging itself back. A plain free scroll
     is what a strip of cards wants. */
  padding-inline: var(--gutter);
  /* Stops a swipe past either end from chaining out to the browser's own
     back-and-forward gesture. */
  overscroll-behavior-x: contain;
}
.journey__stages::-webkit-scrollbar { display: none; }

/* Draggable wherever there is a mouse, including a narrowed window. */
html[data-journey] .journey__stages { cursor: grab; user-select: none; }
/* The photographs are part of the row, not things to be dragged out of it. */
html[data-journey] .journey__stages img { -webkit-user-drag: none; user-select: none; }
html[data-journey] .journey__stages[data-dragging="true"] { cursor: grabbing; }
html[data-cursor="on"] .journey__stages,
html[data-cursor="on"] .journey__stages * { cursor: none; }
.journey__stage { position: relative; flex: 0 0 52vw; }

/* The line every card hangs from, drawn across each gap. */
.journey__stage + .journey__stage::before {
  content: "";
  position: absolute;
  top: 50%;
  left: calc(var(--jgap) * -1);
  width: var(--jgap);
  height: 1px;
  background: color-mix(in srgb, var(--canvas) 72%, transparent);
}

@media (min-width: 56rem) {
  .journey__stage { flex: 0 0 clamp(15rem, 21vw, 23rem); }

  /* Once it is moving of its own accord, snapping would fight the drift. */

  html[data-cursor="on"] .journey__stages,
  html[data-cursor="on"] .journey__stages * { cursor: none; }

  /* A mark at the right-hand edge, to say that this moves. */
  .journey__hint {
    position: absolute;
    top: 50%;
    right: 0;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.9rem 0 0.9rem 5rem;
    font-family: var(--mono);
    font-size: var(--t-label);
    letter-spacing: 0.13em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--canvas) 60%, transparent);
    pointer-events: none;
    background: linear-gradient(to right, transparent, var(--ground) 62%);
  }
  .journey__hint .arrow { --arrow-w: 2.2em; }
}
@media (max-width: 56rem) { .journey__hint { display: none; } }

/* ---- Phones: one swipeable row, the line running through it ------------ */

@media (max-width: 56rem) {
  .journey__track { padding-inline: 0; }
  .journey__grid { display: block; }
  /* Only the gap changes; the connector drawn across it follows from it. */
  .journey__stages { --jgap: 1.25rem; }
  .journey__stage { flex: 0 0 52vw; }
}

/* --------------------------------------------------------------------------
   8b. The jump filter — one line, sticky under the masthead
   -------------------------------------------------------------------------- */

/* Takes the place of a list of services at the head of the page: it costs a
   single line, and it goes on being useful all the way down. */
.filter {
  position: sticky;
  top: var(--masthead-h);
  z-index: 30;
  background: color-mix(in srgb, var(--ultramarine) 92%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-block: 1px solid var(--rule);
}

.filter__inner {
  display: flex;
  align-items: baseline;
  gap: clamp(1rem, 2.4vw, 2rem);
  padding-block: 0.7rem;
}

.filter__label {
  flex: none;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.filter__links {
  display: flex;
  gap: clamp(0.9rem, 2vw, 1.9rem);
  /* On a narrow screen the row scrolls rather than wrapping, so the bar stays
     one line high whatever the width. */
  overflow-x: auto;
  scrollbar-width: none;
  overscroll-behavior-x: contain;
}
.filter__links::-webkit-scrollbar { display: none; }

/* The fade only belongs where the row can actually run off the end. On a wide
   screen it would sit over the last label for no reason. */
@media (max-width: 56rem) {
  .filter__links {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 2rem), transparent);
  }
}

.filter__links a {
  flex: none;
  font-family: var(--sans);
  font-size: var(--t-small);
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  color: var(--ink-strong-muted);
  border-bottom: 1px solid transparent;
  padding-bottom: 2px;
  transition: color 0.25s ease, border-color 0.25s ease;
}
.filter__links a:hover { color: var(--accent); }
/* Set from the script, so with it absent this is simply a contents list. */
.filter__links a[data-current="true"] {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

@media (max-width: 40rem) {
  .filter__label { display: none; }
}

/* --------------------------------------------------------------------------
   9. Specification lists
   -------------------------------------------------------------------------- */

/* Specification lists — the plain facts, set apart from the prose. */
.spec { display: grid; gap: 0.55rem; align-content: start; }
.spec li {
  display: grid;
  grid-template-columns: 0.6rem 1fr;
  gap: 0.75rem;
  font-family: var(--sans);
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink-strong-muted);
}
.spec li::before {
  content: "";
  width: 0.6rem; height: 1px;
  background: var(--ink-strong-muted);
  transform: translateY(0.7em);
}

.svc-row {
  --svc-shell: 87.5rem;                       /* 1400px */
  /* The picture's width on a desktop. At half the row it stood 640px tall
     against two or three lines of text, which is what made these pages feel
     mostly empty and take so long to scroll. */
  --svc-plate: clamp(17rem, 24vw, 21rem);
  width: min(100% - (var(--gutter) * 2), var(--svc-shell));
  margin-inline: auto;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.1rem, 2vw, 1.6rem);
  padding-block: clamp(1.2rem, 0.75rem + 1.2vw, 1.8rem);
}

/* A short rule at the head of each row, set to the content grid rather than
   ruled across the whole page. */
.svc-row::before {
  content: "";
  display: block;
  width: 4.5rem;
  height: 1px;
  background: var(--rule-strong);
  margin-bottom: clamp(0.9rem, 1.6vw, 1.4rem);
}

.svc-row__head .ledger {
  display: block;
  margin-bottom: 0.6rem;
  color: var(--ink-strong-muted);
}
.svc-row__head h2,
.svc-row__head h3 { font-size: var(--t-title); margin-bottom: 0.9rem; }
.svc-row__head p { max-width: 34ch; }        /* a comfortable line */
.svc-row__head p + p { margin-top: 0.8em; }

/* A case study runs to two paragraphs, so its picture is sized to stand beside
   them rather than to the service entries' single sentence. */
.svc-row--case { --svc-plate: clamp(20rem, 34vw, 30rem); }

.svc-row__figure { margin: 0; }
.svc-row__figure .plate { width: 100%; }

/* The photographs are portrait, so a plate given the full half-width would
   stand about twice the height of the words beside it. It is held to a plate
   width instead and pushed out to the edge of the row, which keeps the two
   columns in proportion and the outer margin straight down the page. */
@media (min-width: 56rem) {
  /* Held to its own size and pushed out to the margin on its own side, so the
     run of pictures reads as one clean edge down the page. */
  .svc-row__figure .plate { max-width: var(--svc-plate); }
  .svc-row[data-side="left"]  .svc-row__figure .plate { margin-left: auto; }
  .svc-row[data-side="right"] .svc-row__figure .plate { margin-right: auto; }

}

.svc-row__tail { display: grid; gap: 1.6rem; align-content: start; justify-items: start; }
.svc-row__tail .spec { margin: 0; }
.svc-row__tail p { max-width: 44ch; }

@media (min-width: 56rem) {
  .svc-row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.12fr);
    column-gap: clamp(2rem, 3.6vw, 3.75rem);
    row-gap: 1.8rem;                 /* a clear step down to the points */
    align-items: start;
    /* The empty fourth row takes up whatever the picture is taller by. Without
       it that slack is shared between the head and tail rows, which pushes the
       points away from the paragraph by a different amount on every section. */
    grid-template-rows: auto auto auto 1fr;
    grid-template-areas:
      "rule  ."
      "head  figure"
      "tail  figure"
      "slack figure";
  }
  .svc-row[data-side="right"] {
    /* The picture column keeps the larger share whichever side it is on, so
       every photograph on the page comes out the same size. */
    grid-template-columns: minmax(0, 1.12fr) minmax(0, 1fr);
    grid-template-rows: auto auto auto 1fr;
    grid-template-areas:
      ".      rule"
      "figure head"
      "figure tail"
      "figure slack";
  }
  .svc-row::before { grid-area: rule; }

  .svc-row__head   { grid-area: head; }
  .svc-row__tail   { grid-area: tail; }
  .svc-row__figure { grid-area: figure; }

  /* One measure for the whole text column. The paragraph used to be capped at
     42ch while the points ran the full width of the column, which left the
     block visibly ragged — a short paragraph above a bullet reaching almost to
     the picture. */
  .svc-row__head p,
  .svc-row__tail { max-width: 36rem; }
}

/* Alternating entries: text and picture swap sides down the page, so the eye
   is carried from one to the next rather than reading a single column.

   The pictures are deliberately modest on a desktop. At half the column width
   they stood 640px tall against two or three lines of text, which is what made
   the page feel mostly empty and take so long to scroll. */

/* --------------------------------------------------------------------------
   9c. The expanding index — Exhibition Services and Collection Management

   These two pages carry a sentence per service, which is too little to hold an
   alternating row and far too little to hold a page of them. Title and services
   run down one side, one standing photograph down the other, and each service
   opens to its copy. It replaces the jump filter on these two pages: a list you
   can see all of at once does not need a contents bar above it.
   -------------------------------------------------------------------------- */

.services-page {
  /* One width for the standing photograph and for the dots under it. */
  --standing-w: min(100%, clamp(18rem, 58vh, 38rem));
  padding-top: clamp(2.5rem, 6vw, 5.5rem);
  padding-bottom: var(--band);
  /* The blue holds the screen whatever the window. Without this the paper panel
     below it climbs up into the first screen on a tall display, and the page
     reads as a short blue band with a white page underneath it. */
  min-height: calc(100vh - var(--masthead-h));
  min-height: calc(100svh - var(--masthead-h));
}
@media (min-width: 56rem) {
  .services-page {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: var(--gutter);
    align-items: start;
  }
}
.services-page__head .ledger { display: block; margin-bottom: 1.4rem; }
.services-page__head h1 { font-size: clamp(2.4rem, 1.1rem + 4.2vw, 4.6rem); }

/* The page's own photographs, one at a time. Every frame is stacked in the same
   plate; the first is the one that shows with the script absent, which is why
   it is the only one that starts opaque. */
/* The plate has to be the containing block. Without this the frames take their
   inset from the page itself, stretch across the whole document and sit on top
   of the services, which swallows every click on them. */
.slideshow { position: relative; }
.slideshow img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
}
.slideshow img:first-child { opacity: 1; }
.slideshow img[data-on="true"]  { opacity: 1; }
.slideshow img[data-on="false"] { opacity: 0; }
@media (prefers-reduced-motion: no-preference) {
  .slideshow img { transition: opacity 0.55s ease; }
}

/* The standing photograph. Its width comes from the height of the window, so it
   fills the screen it is on rather than the column it is in — the same trick as
   the About portrait. */
.services-page__figure { margin: 0; }
@media (min-width: 56rem) {
  .services-page__figure .plate {
    width: var(--standing-w);
    margin-left: auto;
  }
}

/* Laid on the foot of the photograph rather than under it. White with a little
   shadow, because what is behind them is a different picture every few seconds
   and cannot be relied on to be dark. They take no clicks: the halves of the
   photograph underneath them do. */
.slideshow__dots {
  position: absolute;
  left: 50%;
  bottom: 0.95rem;
  transform: translateX(-50%);
  z-index: 4;
  display: flex;
  gap: 0.5rem;
  pointer-events: none;
}
.slideshow__dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--canvas);
  opacity: 0.45;
  filter: drop-shadow(0 1px 2px rgba(15, 27, 112, 0.55));
  transition: opacity 0.35s ease;
}
.slideshow__dot[data-on="true"] { opacity: 1; }

/* The left half steps back, the right half steps on. They carry no mark of
   their own — the dots say there is a set, and the pointer says it can be
   acted on. */
.slideshow__zone {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50%;
  z-index: 3;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.slideshow__zone--prev { left: 0; }
.slideshow__zone--next { right: 0; }
@media (max-width: 55.999rem) { .services-page__figure { margin-top: var(--band); } }

.services { margin-top: clamp(2rem, 4vw, 3.25rem); border-top: 1px solid var(--rule); }

.service { border-bottom: 1px solid var(--rule); }

.service > summary {
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 0.9rem;
  padding-block: 0.95rem;
  cursor: pointer;
  list-style: none;                     /* the disclosure triangle, removed */
  font-family: var(--sans);
  font-size: var(--t-body);
  letter-spacing: -0.016em;
  color: var(--ink-strong-muted);
  transition: color 0.25s ease;
}
.service > summary::-webkit-details-marker { display: none; }
.service > summary:hover,
.service[open] > summary { color: var(--accent); }

.service__index {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.1em;
  color: var(--ink-muted);
}

/* Two rules crossed; the upright one turns flat when the entry opens, so the
   plus becomes a minus without a second glyph or an icon font. */
.service__sign {
  position: relative;
  width: 0.75rem; height: 0.75rem;
  align-self: center;
}
.service__sign::before,
.service__sign::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: currentColor;
}
.service__sign::after {
  transform: rotate(90deg);
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.service[open] .service__sign::after { transform: rotate(0deg); }

.service__body {
  padding: 0.1rem 0 1.75rem;
  display: grid;
  gap: 1.15rem;
}
/* Set under the title rather than under the number, so the opened copy lines up
   with the heading it belongs to. */
@media (min-width: 40rem) { .service__body { padding-left: 3.9rem; } }
.service__body p { max-width: 42ch; }

/* --------------------------------------------------------------------------
   10. Ultramarine panel — one caesura per page
   -------------------------------------------------------------------------- */

.panel { padding-block: var(--band); }
.overview p { font-size: var(--t-lead); line-height: 1.5; max-width: 38em; }
.overview p + p { margin-top: 1.3em; }

/* Location takes the charcoal, which carries white at 15:1 and gives the page
   a third value between the slate above it and the paper below. */
.panel--charcoal {
  --ground: var(--charcoal);
  --ink: var(--canvas);
  --ink-muted:        color-mix(in srgb, var(--canvas) 66%, var(--charcoal));
  --ink-strong-muted: color-mix(in srgb, var(--canvas) 86%, var(--charcoal));
  --accent: var(--canvas);
  --rule:        rgba(253, 253, 253, 0.22);
  --rule-strong: rgba(253, 253, 253, 0.5);
  --plate-bg: color-mix(in srgb, var(--canvas) 12%, var(--charcoal));
}

/* The overview takes the third brand value as its ground, set in white.
   NOTE: slate is mid-toned, so white sits at about 2.8:1 against it — under
   the 4.5:1 every other surface on this site holds to. It is the client's
   call. If it ever needs to pass, the ground has to come down to about
   #767676; the ink does not have anywhere left to go. */
.panel--slate {
  --ground: var(--slate);
  --ink: var(--canvas);
  --ink-muted:        color-mix(in srgb, var(--canvas) 82%, var(--slate));
  --ink-strong-muted: color-mix(in srgb, var(--canvas) 92%, var(--slate));
  --accent: var(--canvas);
  --rule:        rgba(253, 253, 253, 0.4);
  --rule-strong: rgba(253, 253, 253, 0.7);
  --plate-bg: color-mix(in srgb, var(--canvas) 14%, var(--slate));
}
/* Where two paper sections run on into each other, and into the footer below
   them, a hairline keeps them from reading as one undivided block. */
.panel + .panel, .panel + .footer { border-top: 1px solid var(--rule); }
.panel a { color: inherit; }
.panel h2 { font-size: var(--t-title); }
.panel p { color: var(--ink-strong-muted); }

/* The facility line — stated conditions, set as a specification. */
.conditions li { display: inline-flex; }
.conditions button {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  border-bottom: 1px solid currentColor;
  transition: color 0.25s ease;
}
.conditions button:hover { color: var(--accent); }

.conditions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 2.5rem;
  margin-top: var(--stack);
  padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-strong-muted);
}

/* --------------------------------------------------------------------------
   11. Figures and image sets
   -------------------------------------------------------------------------- */

figure { margin: 0; }

/* Sized from the height of the window rather than the width of its column, so
   it fills the screen it is on: the plate is 3:4, so a width of 46vh stands
   about 61vh tall and sits on the centre line with room above and below. */
.portrait {
  /* 6rem is held back from the column for the signature, which hangs 17% of the
     plate past its right edge. Without it a bigger plate pushed the flourish
     off the side of the page and the whole document scrolled sideways —
     measured at 37px over at 480px wide. */
  max-width: min(100% - 6rem, clamp(20rem, 46vh, 30rem));
  margin-inline: auto;
}
.portrait__figure { position: relative; }

/* The name signed across the corner of the plate and off the edge of it, so
   the photograph is not a sealed rectangle. The corner of this portrait is
   almost black and the page behind it is the ultramarine, so one white ink
   reads on both. It is decoration — the name is in the caption for anything
   that is reading rather than looking. */
.signature {
  position: absolute;
  right: -17%;
  bottom: -3%;
  width: 95%;
  color: var(--canvas);
  pointer-events: none;
  /* Just enough shadow to hold the stroke together if the photograph is ever
     swapped for a lighter one. */
  filter: drop-shadow(0 1px 2px rgba(15, 27, 112, 0.45));
}
.signature__ink { width: 100%; height: auto; display: block; overflow: visible; }

/* Written rather than revealed. pathLength="1" normalises each path to a length
   of one, so a single dash covers it and the offset can run from 1 to 0 — no
   getTotalLength, no script. The two paths are the two words, so Henderson
   starts after Fred has finished. Outside the guard the signature is simply
   there, drawn, which is the right resting state. */
@media (prefers-reduced-motion: no-preference) {
  .signature__ink path {
    stroke-dasharray: 1;
    stroke-dashoffset: 1;
    animation: sign-on 1.5s cubic-bezier(0.4, 0.1, 0.4, 1) both;
  }
  /* Straight away, not after a pause: the page opens and the name is being
     written. Henderson picks up as Fred finishes. */
  .signature__ink .sig-a { animation-delay: 0s;    animation-duration: 1.35s; }
  .signature__ink .sig-b { animation-delay: 1.2s;  animation-duration: 1.95s; }
  @keyframes sign-on { to { stroke-dashoffset: 0; } }
}

/* On a phone the plate fills its column, so the overhang has to stay inside
   the gutter or it drags the whole page sideways. */
@media (max-width: 27rem) {
  .signature { right: -4%; bottom: 0; width: 86%; }
}
.plate { background: var(--plate-bg); overflow: hidden; }
.plate img { width: 100%; }
.plate--tall { aspect-ratio: 3 / 4; }
.plate--tall img { height: 100%; object-fit: cover; }
/* The 2025 photography is all 3:4, so the plates that carry it are cut to
   that or to 4:5, rather than to a landscape frame they would be gutted for. */
.plate--r45 { aspect-ratio: 4 / 5; }
.plate--r45 img { height: 100%; object-fit: cover; }

/* --------------------------------------------------------------------------
   12. Links, actions
   -------------------------------------------------------------------------- */

.link {
  font-family: var(--sans);
  font-size: var(--t-small);
  text-decoration: none;
  border-bottom: 1px solid var(--rule-strong);
  padding-bottom: 1px;
  transition: border-color 0.25s ease, color 0.25s ease;
  display: inline-block;
}
.link:hover { color: var(--accent); border-bottom-color: var(--accent); }

.link--trail {
  --arrow-w: 3.4em;
  display: inline-flex;
  align-items: center;
  gap: 0.9rem;
  font-family: var(--sans);
  font-size: var(--t-body);
  text-decoration: none;
  border: 0;
  transition: color 0.25s ease;
}
.link--trail:hover { color: var(--accent); }
.link--trail .arrow { transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); }
.link--trail:hover .arrow { transform: translateX(0.3rem); }

/* A filled button. It takes the ink of whatever ground it is on and prints the
   ground back out of it, so it reverses cleanly on the paper sections. */
.btn {
  --arrow-w: 1.6em;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.62rem 1.05rem;
  border: 1px solid var(--ink);
  background: var(--ink);
  color: var(--ground);
  font-family: var(--sans);
  font-size: var(--t-small);
  letter-spacing: -0.01em;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.25s ease, color 0.25s ease;
}
.btn:hover { background: var(--ink-strong-muted); border-color: var(--ink-strong-muted); }
.btn .arrow { transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); }
.btn:hover .arrow { transform: translateX(0.25rem); }
.btn--self { justify-self: start; }

.action {
  display: inline-flex;
  align-items: baseline;
  gap: 0.9rem;
  font-family: var(--sans);
  font-size: clamp(1.1rem, 0.95rem + 0.8vw, 1.6rem);
  letter-spacing: -0.021em;
  text-decoration: none;
  color: var(--ink);
  border-bottom: 1px solid currentColor;
  padding-bottom: 0.15em;
}
.arrow {
  display: inline-block;
  position: relative;
  width: var(--arrow-w, 1.9em);
  height: 0.8em;
  flex: none;
  vertical-align: middle;
}
/* The shaft, on the vertical centre of the element. */
.arrow::before {
  content: "";
  position: absolute;
  top: 50%; left: 0; right: 0;
  height: 1px;
  margin-top: -0.5px;
  background: currentColor;
}
/* The head: a square turned about its own centre, so its vertex lands exactly
   on the end of the shaft. Offset by 0.207 x its side, which is how far a
   45-degree rotation pushes the corner past the box. */
.arrow::after {
  content: "";
  position: absolute;
  top: 50%; right: 0.091em;
  width: 0.44em; height: 0.44em;
  margin-top: -0.22em;
  border-top: 1px solid currentColor;
  border-right: 1px solid currentColor;
  transform: rotate(45deg);
}
.action .arrow { transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); }
.action:hover .arrow { transform: translateX(0.35rem); }

/* As a case study is revealed, its arrow draws: the shaft runs out from the
   words and the head follows it. Scoped to .js and inside the motion guard for
   the same reason the reveal itself is — the state in the stylesheet is the
   finished one, so no script and no animation still leaves a drawn arrow. */
/* The link carries its own data-reveal rather than inheriting its row's. A case
   study is a screen and a half tall, and the observer fires at 8% of it, so
   keying the arrow to the row drew it while the link was still well below the
   fold — the animation was over before it came into view. Watching the link
   itself is what puts it on screen at the moment it draws. */
@media (prefers-reduced-motion: no-preference) {
  .js .link--trail[data-reveal] .arrow::before {
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.75s cubic-bezier(0.22, 1, 0.36, 1) 0.22s;
  }
  .js .link--trail[data-reveal] .arrow::after {
    opacity: 0;
    transform: translateX(-0.55em) rotate(45deg);
    transition: opacity 0.45s ease 0.5s,
                transform 0.75s cubic-bezier(0.22, 1, 0.36, 1) 0.34s;
  }
  .js .link--trail[data-reveal][data-shown="true"] .arrow::before { transform: scaleX(1); }
  .js .link--trail[data-reveal][data-shown="true"] .arrow::after {
    opacity: 1;
    transform: translateX(0) rotate(45deg);
  }
}

/* Directory of onward pages */
.directory { border-top: 1px solid var(--rule); }
.directory a {
  display: grid;
  grid-template-columns: 3.25rem 1fr auto;
  gap: 1rem;
  align-items: baseline;
  padding-block: clamp(1.25rem, 2.4vw, 1.9rem);
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  font-family: var(--sans);
  font-size: clamp(1.15rem, 0.95rem + 1vw, 1.8rem);
  letter-spacing: -0.023em;
  transition: color 0.25s ease;
}
.directory a:hover { color: var(--accent); }
.directory .arrow { transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); }
.directory a:hover .arrow { transform: translateX(0.35rem); }

/* --------------------------------------------------------------------------
   12b. Case studies and the map
   -------------------------------------------------------------------------- */

/* The map is fetched only when someone asks to see it. */
.map-dialog, .map-dialog * { cursor: auto; }
.map-dialog button { cursor: pointer; }

.map-dialog {
  width: min(92vw, 56rem);
  max-height: 88vh;
  padding: 0;
  border: 0;
  box-shadow: 0 2rem 5rem rgba(15, 27, 112, 0.4);
}
.map-dialog::backdrop { background: rgba(15, 27, 112, 0.62); }
.map-dialog__bar {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1rem clamp(1rem, 2vw, 1.5rem);
  border-bottom: 1px solid var(--rule);
}
.map-dialog__bar p { margin: 0; max-width: none; }
.map-dialog__frame { aspect-ratio: 16 / 10; background: var(--plate-bg); }
.map-dialog__frame { position: relative; }
.map-dialog__frame iframe { width: 100%; height: 100%; border: 0; display: block; }
/* We publish the district rather than the street, so the map is marked as an
   area. The ring is drawn over the tiles and never takes the pointer. */
.map-dialog__frame::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: min(42%, 15rem); aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border: 1px solid color-mix(in srgb, var(--ultramarine) 70%, transparent);
  background: color-mix(in srgb, var(--ultramarine) 12%, transparent);
  border-radius: 50%;
  pointer-events: none;
}
.map-dialog__frame:empty::after { display: none; }
.map-dialog__fallback { padding: 0.9rem clamp(1rem, 2vw, 1.5rem); margin: 0; }

button.link { background: none; border: 0; border-bottom: 1px solid var(--rule-strong); cursor: pointer; font: inherit; color: inherit; }
button.link { font-family: var(--sans); font-size: var(--t-small); }

/* Contact and About are short enough to hold one screen, so their title sits
   beside the content rather than above it. */
.page--single {
  display: flex;
  flex-direction: column;
  /* One screen of content. The footer sits below it and is scrolled to. */
  min-height: calc(100vh - var(--masthead-h));
  min-height: calc(100svh - var(--masthead-h));
}
.page--single .page-head {
  padding-top: clamp(1.5rem, 3.5vh, 3rem);
  padding-bottom: 0;
}
.page--single .page-head h1 { font-size: clamp(1.9rem, 1.1rem + 2.6vw, 3.2rem); }
.page--single .page-head .lead { margin-top: clamp(0.75rem, 1.6vh, 1.25rem); font-size: var(--t-body); }
.page--single > .band {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding-block: clamp(1.75rem, 4.5vh, 3.5rem);
}

.single-grid {
  display: grid;
  gap: var(--stack) var(--gutter);
  grid-template-columns: 1fr;
  align-items: start;
}

/* On a screen tall enough to hold the page, the columns stretch and their
   contents spread down it rather than bunching at the top. */
@media (min-width: 62rem) {
  .single-grid { flex: 1; align-items: stretch; }
  /* The portrait keeps its own block; only the text stacks become flex.
     Content groups at the top of its column; the enquiry list is the one thing
     that stretches, so the column still reaches the foot of the screen. */
  .single-grid > div:not(.portrait) {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
  }
  .single-grid > div > :first-child { margin-top: 0; }

  /* The rows keep their own rhythm rather than sharing out the height of the
     column. Stretched to fill, each one stood 90px tall with the words floating
     in the middle of it, which read nothing like the ruled lists on the service
     pages — those are set by their padding and come out around 54px. Same
     padding here, same line. */
  .single-grid .enquiries a { align-items: baseline; padding-block: 0.95rem; }

  /* The portrait takes whatever height is left. */
  .single-grid--about .portrait { align-self: center; }
}
@media (min-width: 62rem) {
  .single-grid--contact { grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr) minmax(0, 1fr); }
  .single-grid--about   { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }
}

.single-title { font-size: clamp(2.1rem, 1rem + 3.4vh, 3.6rem); margin-top: clamp(0.6rem, 1.4vh, 1rem); }

/* Contact is meant to hold one screen. The photograph is therefore sized from
   the height that is going spare rather than from its own ratio, which is what
   was pushing the page over the fold on a short window. */
.single-grid--contact .plate {
  aspect-ratio: auto;
  height: clamp(6rem, 22vh, 15rem);
}
.single-grid--contact .plate img { height: 100%; object-fit: cover; }
.single-grid--contact figure { margin: 0 0 clamp(0.75rem, 1.6vh, 1.25rem) !important; }
.single-grid--contact address { margin-bottom: clamp(0.75rem, 1.6vh, 1.25rem) !important; }

.contact-details { display: grid; gap: 0 var(--gutter); }

@media (max-width: 62rem) {
  /* One column of eight, the same as everywhere else. It was two of four, which
     halved the height but read as a grid of tiles rather than a list. */
  .single-grid--contact .enquiries a { padding-block: clamp(0.5rem, 1.1vh, 0.9rem); }
  .contact-details { grid-template-columns: 1fr 1fr; }
  .page--single > .band { padding-block: clamp(1.25rem, 2.8vh, 2.25rem); }
}

/* A short window — a small phone, or a laptop with a lot of browser chrome —
   gets one more turn of the screw before the page is allowed to scroll. */
@media (max-height: 820px) {
  .page--single > .band { padding-block: clamp(0.75rem, 2vh, 1.5rem); }
  .single-grid--contact { row-gap: 0.75rem; }
  .single-grid--contact .enquiries a { padding-block: 0.38rem; }
  .single-grid--contact .label { margin-bottom: 0.4rem !important; }
  .single-grid--contact address { line-height: 1.45; }
  .single-grid--contact .plate { height: clamp(5rem, 18vh, 10rem); }
}

@media (max-width: 30rem) {
  /* The photograph is the one thing here carrying no information. */
  .single-grid--contact figure { display: none; }
  /* Everything else gives up a little of its air so the page still lands on
     one screen on a phone. */
  .page--single > .band { padding-block: clamp(1rem, 2.4vh, 1.75rem); }
  .single-grid--contact { row-gap: 1rem; }
  .single-grid--contact .single-title + p { font-size: var(--t-small); line-height: 1.5; }
  .single-grid--contact .enquiries a { padding-block: 0.5rem; }
  .single-grid--contact address { line-height: 1.5; }
}

/* Eight enquiry rows have to share the screen with everything else, so they
   scale with viewport height as well as width. */
.enquiries a { padding-block: clamp(0.6rem, 1.5vh, 1.05rem); line-height: 1.35; }

/* --------------------------------------------------------------------------
   13. Contact
   -------------------------------------------------------------------------- */

.enquiries { display: grid; gap: 0; border-top: 1px solid var(--rule); }
.enquiries a {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 1rem;
  align-items: baseline;
  padding-block: 0.72rem;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  font-family: var(--sans);
  font-size: var(--t-body);
  letter-spacing: -0.014em;
  transition: color 0.25s ease;
}
.enquiries a:hover { color: var(--accent); }
.enquiries .arrow { color: var(--ink-muted); transition: transform 0.3s ease, color 0.25s ease; }
.enquiries a:hover .arrow { color: var(--accent); transform: translateX(0.3rem); }

address {
  font-style: normal;
  line-height: 1.7;
}

/* --------------------------------------------------------------------------
   14. Footer
   -------------------------------------------------------------------------- */

.footer {
  padding-block: clamp(1.25rem, 2.6vh, 2.5rem) clamp(0.75rem, 1.4vh, 1.25rem);
}
.footer__grid {
  display: grid;
  gap: clamp(1rem, 2vh, 1.5rem) var(--gutter);
  grid-template-columns: 1fr;
}
@media (min-width: 46rem) { .footer__grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 68rem) { .footer__grid { grid-template-columns: 1.4fr 1fr 1fr 1.2fr; } }

.footer h3 {
  font-size: var(--t-label); font-family: var(--mono); letter-spacing: 0.13em;
  text-transform: uppercase; font-weight: 400; margin-bottom: 0.75rem;
  color: var(--ink-muted);
}
.footer ul { display: grid; gap: 0.3rem; }
.footer a { font-family: var(--sans); font-size: var(--t-small); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.25s ease; }
.footer a:hover { border-bottom-color: var(--rule-strong); }
.footer .wordmark { width: 10rem; margin-bottom: 0.9rem; }
.footer .label, .footer address { color: var(--ink-muted); }

.colophon {
  margin-top: clamp(0.9rem, 2vh, 1.75rem);
  padding-top: clamp(0.7rem, 1.4vh, 1.1rem);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 2rem;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: 0.07em;
  color: var(--ink-muted);
}
.colophon p { margin: 0; max-width: none; }

/* --------------------------------------------------------------------------
   14b. The pointer — the dot from Dougal's holding page, turned over with
        the rest of the palette: paper on the blue, blue on the paper
   -------------------------------------------------------------------------- */

/* Gated three ways: a real mouse, scripting available, and motion allowed.
   The native cursor is only hidden once the dot is confirmed running, so a
   script failure can never leave someone without a pointer. */
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  html[data-cursor="on"] body { cursor: none; }
  html[data-cursor="on"] a,
  html[data-cursor="on"] button,
  html[data-cursor="on"] summary,
  html[data-cursor="on"] [role="button"] { cursor: none; }

  .cursor-dot {
    position: fixed;
    top: 0; left: 0;
    z-index: 90;
    width: 9px; height: 9px;
    margin: -4.5px 0 0 -4.5px;
    border-radius: 50%;
    background: var(--canvas);
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s ease, width 0.28s cubic-bezier(0.22, 1, 0.36, 1),
                height 0.28s cubic-bezier(0.22, 1, 0.36, 1),
                margin 0.28s cubic-bezier(0.22, 1, 0.36, 1),
                background-color 0.28s ease;
    /* Its own layer, so moving it never asks for a repaint of the page. */
    will-change: transform;
    transform: translateZ(0);
    contain: layout style paint;
  }
  html[data-cursor="on"] .cursor-dot { opacity: 1; }

  /* It opens up over anything you can act on. */
  .cursor-dot[data-over="link"] {
    width: 22px; height: 22px;
    margin: -11px 0 0 -11px;
    background: color-mix(in srgb, var(--canvas) 28%, transparent);
  }
  /* Over the paper sections it inverts, so it stays visible either way. */
  .cursor-dot[data-ground="paper"] { background: var(--ultramarine); }
  .cursor-dot[data-ground="paper"][data-over="link"] {
    background: color-mix(in srgb, var(--ultramarine) 34%, transparent);
  }
}

/* --------------------------------------------------------------------------
   15. Motion — slow, and easily declined
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
  /* Scoped to .js so that a script failure can never leave content invisible. */
  .js [data-reveal] {
    opacity: 0;
    transform: translateY(1.1rem);
    transition: opacity 0.85s cubic-bezier(0.22, 1, 0.36, 1),
                transform 0.85s cubic-bezier(0.22, 1, 0.36, 1);
  }
  .js [data-reveal][data-shown="true"] { opacity: 1; transform: none; }

  @view-transition { navigation: auto; }
  ::view-transition-old(root) { animation-duration: 0.24s; }
  ::view-transition-new(root) { animation-duration: 0.34s; }
}

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

/* --------------------------------------------------------------------------
   16. Print — the site as a document, because this trade still prints things
   -------------------------------------------------------------------------- */

@media print {
  /* On paper the site is a document: black on white, whichever way the
     screen palette happens to be turned. */
  :root, .on-paper, .panel, .footer, .menu, .map-dialog {
    --ground: #fff; --ink: #000; --ink-muted: #333;
    --ink-strong-muted: #222; --accent: #000;
    --rule: #ccc; --rule-strong: #999; --plate-bg: #fff;
  }
  body { background: #fff; color: #000; font-size: 10.5pt; line-height: 1.45; }
  .band { padding-block: 0.6cm; }
  .masthead, .panel, .footer, .menu { background: #fff !important; color: #000 !important; }
  .panel { border-block: 1px solid #000; }
  .btn { background: #fff !important; color: #000 !important; border-color: #000 !important; }
  .filter { display: none; }
  figure, .plate { break-inside: avoid; }
  img { max-height: 8cm; object-fit: contain; }
  a { text-decoration: none; }
  .main a[href^="http"]::after,
  .main a[href^="mailto"]::after { content: " (" attr(href) ")"; font-size: 8pt; color: #555; }
  .footer { margin-top: 0.8cm; }
}
