@charset "utf-8";

/* ==========================================================================
   Finch + Finch
   Design system v1, mirrored from the Design Code artifact and the
   "f+f Strategic Real Estate" one-pager. Light theme only.
   ========================================================================== */

:root {
  /* Brand */
  --slate:  #465362;
  --navy:   #061B3A;
  --teal:   #2FA29E;
  --teal-d: #26837F;   /* teal darkened to 4.6:1, for link text */
  --taupe:  #C5AFA4;
  --rose:   #CC7E85;

  /* Surfaces.
     surface-cream (#FAF0CA) is deliberately ABSENT. It is in the design system
     but reads as yellow on screen at any size, so nothing here uses it. Where
     a warm pale ground is wanted, --pale does the job. */
  --page:   #FDF9F0;
  --pale:   #F0ECE4;
  --grey:   #F2F2F2;
  --white:  #FFFFFF;

  --ink:    #465362;
  --ink-2:  #5D6B7A;

  /* Type */
  --sans: "Tw Cen MT", "Century Gothic", "Futura", "Trebuchet MS", sans-serif;

  /* Spacing */
  --s1: 4px;  --s2: 8px;  --s3: 16px;
  --s4: 24px; --s5: 32px; --s6: 48px;

  /* Radius */
  --r-sm: 4px; --r-md: 8px; --r-lg: 16px; --r-full: 9999px;

  --wrap: 1120px;

  /* The reading measure. Anything that holds a paragraph is held to it, so a
     callout cannot end up wider than the prose it sits beside — which is exactly
     what happened when .prose owned the only copy of this number and .msg had
     none. One token, two users, no drift. */
  --measure: 68ch;

  --skyline-foot: 20px;  /* fixed clearance from the skyline to the footer */
}

/* --------------------------------------------------------------- reset --- */

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

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

body {
  font-family: var(--sans);
  font-size: 15px;
  line-height: 22px;
  color: var(--ink);
  background: var(--page);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg { display: block; max-width: 100%; }
button, input, textarea, select { font: inherit; color: inherit; }

a { color: var(--teal-d); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--slate); }

/* --- the published address ------------------------------------------------
   ff_email() writes the address backwards and gives the anchor no href, so a
   harvester reading the source or the text finds neither an address nor a
   mailto:. This puts the characters back the right way round for a reader.
   site.js then replaces the span with plain text and adds the real href, so
   after script the rule below simply has nothing left to act on.
   Until then it is an <a> with no href, which is correctly not focusable and
   not clickable — legible and selectable, but not a link. */
.mailto { color: var(--teal-d); }
.mailto[href]:hover { color: var(--slate); }
.mailto__r {
  unicode-bidi: bidi-override;
  direction: rtl;
  /* Selecting it copies the reversed string; that is the trade, and the JS
     path — which is nearly everybody — never sees this. */
  white-space: nowrap;
}

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

.skip {
  position: absolute; left: var(--s3); top: -100px; z-index: 100;
  background: var(--slate); color: var(--white);
  padding: var(--s2) var(--s3); border-radius: var(--r-sm);
  text-decoration: none; transition: top .15s;
}
.skip:focus { top: var(--s3); color: var(--white); }

/* ?me=on is active in this browser (ff_is_me()). Only ever rendered for us.
   Clicks pass straight through, so it can never cover the navigation. */
.me-flag {
  position: fixed; top: var(--s2); right: var(--s2); z-index: 200;
  display: flex; align-items: center; gap: var(--s1);
  padding: 2px var(--s2); border-radius: var(--r-full);
  background: var(--slate); color: var(--white);
  font-size: 12px; line-height: 16px; font-weight: 600; letter-spacing: 1.5px;
  text-transform: uppercase;
  box-shadow: 0 0 0 2px var(--teal);
  pointer-events: none;
}

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

/* ---------------------------------------------------------------- type --- */

h1, h2, h3 { font-weight: 400; }

.display   { font-size: clamp(2.25rem, 5.5vw, 3.5rem); line-height: 1.08; font-weight: 400; }
.heading-1 { font-size: 28px; line-height: 34px; }
.heading-2 { font-size: 18px; line-height: 24px; font-weight: 600; }

.eyebrow {
  font-size: 12px; line-height: 16px; font-weight: 600;
  letter-spacing: 1.5px; text-transform: uppercase;
}
.caption { font-size: 12px; line-height: 16px; }

/* ------------------------------------------------------------ masthead --- */

.masthead {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s4);
  width: min(var(--wrap), 100% - 2 * var(--s4));
  margin-inline: auto;
  padding-block: var(--s4);
  position: relative; z-index: 3;
}

.wordmark {
  display: flex; align-items: center; gap: var(--s3);
  text-decoration: none; color: inherit;
}
/* aspect-ratio rather than width/height attributes: the two marks are the same
   shape, and this reserves the space before either has loaded. */
.wordmark__img { height: 48px; width: auto; aspect-ratio: 2 / 1; }

.wordmark__rule { width: 1px; align-self: stretch; background: currentColor; opacity: .35; }
.wordmark__desc {
  font-size: 14px; line-height: 17px; font-weight: 600;
  letter-spacing: 1.6px; text-transform: uppercase;
}

@media (max-width: 599px) {
  .wordmark__img { height: 40px; }
  .wordmark__desc { font-size: 12px; line-height: 15px; }
}

.backlink {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-size: 13px; text-decoration: none; color: var(--ink-2);
  padding-block: var(--s2);
}
.backlink:hover { color: var(--slate); }
.backlink:hover .backlink__arrow { transform: translateX(-3px); }
.backlink__arrow { transition: transform .18s ease; }
.backlink__short { display: none; }   /* phones only */

/* On the slate band the masthead sits reversed. */
.band .masthead, .band .wordmark, .band .backlink { color: var(--white); }
.band .backlink { color: rgba(255,255,255,.75); }
.band .backlink:hover { color: var(--white); }

/* ============================================================== HOME ===== */

/* Sky.
   A panel, not a page background. It runs from the top of the page to the top
   of the footer — site.js measures that and writes --sky-h — so the photograph
   always fills everything above the footer, whether the content is shorter than
   the screen or taller than it. */
.home {
  min-height: 100svh;
  display: flex; flex-direction: column;
  position: relative;
  background: var(--page);
}

/* The sky is a real element, not a background on <body> and not a negative
   z-index pseudo-element. Both of those were unreliable: a background set on
   body propagates to the canvas, which changes how it is sized and painted,
   and a -1 child then sits behind it. An ordinary div cannot be lost that way.
   Its height is --sky-h, which site.js measures from the skyline's ground line,
   so the sky ends exactly where the buildings stand. */
.sky {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: var(--sky-h, 72vh);
  z-index: 0;
  pointer-events: none;

  /* Longhand, not the shorthand: a var() carrying a url() inside a multi-layer
     background shorthand is fragile to parse, and the size was being lost.
     --sky-img is set on the element by index.php, which checks which path the
     file actually exists at. The gradient underneath is both a fallback and an
     underlay: it shows before the photograph loads, and behind it wherever the
     crop leaves a gap, so the hero is never briefly white type on white. */
  background-color: #1B2634;
  background-image:
    var(--sky-img, none),
    linear-gradient(to bottom, #1B2634 0%, #3A4757 60%, #C89A63 100%);
  background-repeat: no-repeat, no-repeat;

  /* Anchored to the BOTTOM of the box. The warm glow at the foot of the crop is
     the lightest part of the frame and the skyline stands on it, so whatever
     gets cropped as the box changes shape must come off the top. */
  background-position: center bottom, center bottom;

  /* cover, so the photograph fills the box in BOTH axes however tall and
     narrow it gets. Fitting to width leaves the foot of a narrow page bare. */
  background-size: cover, cover;

  /* A floor, so a bad measurement can never collapse the sky to nothing. */
  min-height: 46vh;
}

/* Everything else stands in front of it. */
.home .masthead, .home__main { position: relative; z-index: 1; }

/* ---- the home page is reversed out over the photograph ------------------ */

/* Scoped to .home only: every other page stays on the light scheme. */
.home .masthead,
.home .wordmark,
.home__strap { color: #FFFFFF; }



/* The skyline is BLACK, not white: it stands against the warm glow at the foot
   of the photograph, which is the lightest part of the frame. The wordmark and
   the hero stay white because they sit high up, against the dark. Every
   building is black for now, doors included; the hover growth is what marks
   a door. */
.home .skyline { --sky-ink: #000000; }


.home__main {
  flex: 1;
  display: grid;
  grid-template-rows: 1fr auto;
  width: 100%;
}

.home__statement {
  width: min(var(--wrap), 100% - 2 * var(--s4));
  margin-inline: auto;
  align-self: center;
  padding-block: var(--s6);
  position: relative;          /* the cloud is positioned against this */
}

/* --- the sentence: this is the navigation -------------------------------- */

/* Four real links in running text. Reversed out over the photograph, like the
   strapline, with the same dark halo so the type holds against the star trails
   whatever falls behind it. */
/* No max-width in ch any more. The sentence is broken where it is written —
   two .pitch__line blocks — and each block is allowed the full width of the
   statement, so "their managers and directors with <word>" stays on one line
   wherever there is room for it. The rotator's width is fixed by site.js to
   the widest candidate, so that line never changes length as the word turns. */
.pitch {
  margin-top: var(--s5);
  max-width: 100%;
  color: #FFFFFF;
  /* Capped a little lower than before: the second clause is long, and at 2rem
     it only fitted on one line above about 1150px. */
  font-size: clamp(1.25rem, 2.2vw, 1.75rem);
  line-height: 1.35;
  text-shadow:
    0 0 14px rgba(12, 20, 30, .9), 0 0 30px rgba(12, 20, 30, .8),
    0 1px 3px rgba(0, 0, 0, .5);
}

.pitch__line { display: block; }

/* Below this the second clause cannot fit however it is set, so the whole
   sentence goes back to being one run of text and wraps like prose. This is the
   "unless the page is very narrow" case. */
/* Why 767 and not lower: the font-size clamp shrinks with the viewport, so the
   second clause keeps fitting on one line a long way down — at 800px it is
   about 620px wide in a 750px measure. Below roughly 700px it stops fitting
   whatever the type does, and the site's existing breakpoint is the honest
   place to hand over rather than a value tuned to one font's metrics. */
@media (max-width: 767px) {
  .pitch { max-width: 26ch; }
  .pitch__line { display: inline; }
}

/* Teal, and nothing else at rest: the underline is off for launch.
   Colour alone is not normally allowed to be the only thing marking a link, so
   the underline comes back on hover and on keyboard focus, and focus-visible
   still draws the site-wide outline. */
.pitch__door {
  color: var(--teal);
  text-decoration: none;
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: color .18s ease;
}
.pitch__door:hover,
.pitch__door:focus-visible,
.pitch__door.is-paired {
  color: #7FD6D2;                 /* lifted, not changed - still the accent */
  text-decoration: underline;
  text-decoration-thickness: 2px;
}

/* --- the word that changes ----------------------------------------------- */

/* site.js measures the widest candidate once and fixes the width, so nothing
   after this point can ever move and the sentence cannot re-wrap. */
/* The clipping box is grown well past the word on all four sides and the extra
   band is masked away, rather than the word being clipped at the edge of its
   own line. The sentence carries a 30px glow against the photograph, and a
   hard-edged overflow cut sliced that glow into a visible dark rectangle. The
   mask is fully opaque across the original 1.35em band, so no glyph — and no
   descender — is touched; it only fades out where there is nothing but shadow.
   Padding and an equal negative margin cancel, so the word sits exactly where
   it always did and "with" keeps its space. */
.rot {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  /* content-box, against the sheet-wide border-box: site.js sets the width to
     the widest word, and that measurement is of the text, not of the padding
     this box now carries. */
  box-sizing: content-box;
  height: 1.35em;
  padding: .5em 0;
  margin: -.5em 0;
  white-space: nowrap;
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0, #000 .5em, #000 1.85em, transparent 2.35em);
          mask-image: linear-gradient(to bottom,
    transparent 0, #000 .5em, #000 1.85em, transparent 2.35em);
}
/* The sideways breathing room is only taken where the heavy three-layer glow
   is — from 1024px up, where the gutters are 24px and an invisible band either
   side of the word cannot push the page wide enough to scroll. Below that the
   glow is the single soft 14px one, which the box's own slack absorbs. */
@media (min-width: 1024px) {
  .rot { padding-inline: 1.25em; margin-inline: -1.25em; }
}

.rot__word {
  display: inline-block;
  will-change: transform, filter;
}

/* The reel. Each spin frame sweeps a word up through the window; the last one
   comes up from below and stops, so the whole thing reads as a wheel slowing
   onto an answer. Three blur strengths rather than one: the vertical-only blur
   is what turns the letters into lines, and it has to come off as the wheel
   slows or the landing looks like a dissolve. */
@keyframes rot-reel { from { transform: translateY(1.05em); }
                        to { transform: translateY(-1.05em); } }
@keyframes rot-land { from { transform: translateY(1.05em); }
                        to { transform: translateY(0); } }

.rot--b3 { filter: url(#ff-reel-3); }
.rot--b2 { filter: url(#ff-reel-2); }
.rot--b1 { filter: url(#ff-reel-1); }

/* The filter definitions are markup, and would otherwise take up a line box. */
.rot__filters { position: absolute; width: 0; height: 0; }

/* The canonical word, for assistive technology: the rotator itself is
   aria-hidden, so nothing is interrupted every few seconds. */
.vh {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

@media (max-width: 1023px) {
  .home__strap { text-shadow: 0 1px 14px rgba(12, 20, 30, .7); }
  .pitch { text-shadow: 0 1px 14px rgba(12, 20, 30, .75); }
}

/* The only type on a 1440px page: let it own the page. The display token is
   sized for statements inside a document, not for this. */
.home__strap {
  max-width: 16ch;
  font-size: clamp(2.5rem, 7vw, 5rem);
  line-height: 1.02;
  letter-spacing: -0.5px;
}

/* ------------------------------------------------------------- skyline --- */

/* Clipped sideways only: the ground runs past both edges so that a building
   pushed aside never bares the end of it, and that overrun must not make the
   page scroll. Upward the svg overflows freely, for the hover growth. */
.skyline { position: relative; width: 100%; overflow-x: clip; }

.skyline__stage { position: relative; }

.skyline__svg {
  width: 100%; height: auto; display: block;
  overflow: visible;
  pointer-events: none;
  --sw: .7px;             /* stroke in drawing units (px = user unit in SVG); site.js
                             sets it from the scale. Keep the unit: see site.js. */
}

/* Hidden until site.js has laid it out and started the drawing, so the
   finished picture never flashes up first. inc/skyline.php adds a noscript
   rule that shows it when there is no JavaScript. */
.skyline:not(.skyline--ready) .skyline__svg { visibility: hidden; }

.skyline__svg path {
  fill: none;
  stroke: var(--sky-ink, #000000);
  stroke-width: var(--sw);
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Half weight in the source artwork; lighter still here, so the dense window
   grids read as texture rather than as more line. */
.skyline__svg .sky__fine { stroke-width: calc(var(--sw) * .2); }


/* The door: the link teal, behind the line art, which quietly marks the four
   audience buildings as the ones that open — on a phone too, with no hover.
   Part-transparent at rest; lit, it comes up to full strength and a blurred
   copy behind it comes up as a glow in the same teal. */
.skyline__svg .sky__door {
  fill: var(--teal-d); stroke: none;
  opacity: .55;
  transition: opacity .24s ease;
}
.slot--lit .sky__door { opacity: 1; }
.skyline__svg .sky__glow {
  fill: var(--teal-d); stroke: none;
  filter: url(#sky-glow);
  opacity: 0;
  transition: opacity .24s ease;
}
.slot--lit .sky__glow { opacity: 1; }

/* While any drawing draws — the home skyline, a band building, the strip at
   the foot of a page. Every stroked path carries pathLength="1", so a dash of
   1 and a gap of 1 is the whole path either way; site.js animates the offset. */
.is-drawing path { stroke-dasharray: 1 1; }

/* The band building and the strip: hidden until site.js has set up their
   drawing, so the finished picture never flashes first. head.php shows them
   when there is no JavaScript. */
.profile:not(.is-ready), .strip__svg:not(.is-ready) { visibility: hidden; }

/* Door hit areas sit over the SVG; each is both the link and its own label. */
.skyline__doors { position: absolute; inset: 0; }

.door {
  position: absolute; top: 0; bottom: 0;
  text-decoration: none;
}

/* No labels at all. The four names live in the home page sentence; a door is a
   hit area over its building and nothing more. */

/* One slot, placed where site.js puts it: --x and --y in drawing units,
   written as px, which inside SVG are user units. */
.slot {
  transform-box: view-box;
  transform-origin: 0 0;
  transform: translate(var(--x, 0px), var(--y, 0px));
}
.slot--off { display: none; }

@media (prefers-reduced-motion: reduce) {
  .skyline__svg .sky__door, .skyline__svg .sky__glow { transition: none; }
}

/* Plain routing list: the accessible and no-JS path. */
.routes-plain {
  list-style: none; padding: 0;
  display: flex; flex-wrap: wrap; gap: var(--s4);
  font-size: 18px; font-weight: 600;
}
.routes-plain a { color: var(--slate); text-decoration: none; border-bottom: 2px solid var(--teal); }
.routes-plain a:hover { color: var(--teal-d); }

/* Hidden but present: screen readers and crawlers read this list, while sighted
   pointer users navigate the skyline itself. <noscript> in head.php restores it
   when there is no JavaScript to compose the doors. */
.routes-plain {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* A fixed distance above the footer, at every width. The skyline is the last
   row of the main grid, which is 1fr auto - so main absorbs all the spare
   height into the hero block above and the skyline stays put. The gap between
   the hero and the line opens up on tall screens; the line does not move. */
.home .skyline { margin-bottom: var(--skyline-foot); }

/* ==================================================== AUDIENCE PAGES ===== */

/* The slate band.
   The one-pager's diagonal cut is deliberately NOT used here: it reads as a
   sweep on a wide, short cover, and as a slipped corner on a tall web band. The
   skewed tag chevrons carry the diagonal idea instead, in one place. */
.band {
  background: var(--slate);
  color: var(--white);
  padding-bottom: var(--s6);
  position: relative;
}
/* Left rule only. The bottom border drew a hard line under the band and made
   it read as a panel rather than as the head of a continuous page. */
.band::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 10px;
  background: var(--teal);
}

.band__inner {
  width: min(var(--wrap), 100% - 2 * var(--s4));
  margin-inline: auto;
  padding-top: var(--s4);
  display: grid; gap: var(--s5);
  grid-template-columns: 1fr auto;
  align-items: end;
}
.band__eyebrow { color: var(--taupe); margin-bottom: var(--s3); }
.band__tagline { color: var(--white); max-width: 22ch; }
.band__sub { color: var(--taupe); margin-top: var(--s3); font-size: 15px; }

.profile { overflow: visible; }
/* Weights in drawing units from --sw, which inc/skyline.php sets for 1.5px on
   screen. Not vector-effect: non-scaling-stroke — that puts the dashes the
   drawing is animated with into screen pixels, and it draws as a shuffle of
   specks instead of a line. */
.profile path {
  fill: none; stroke: currentColor; stroke-width: var(--sw, .85px);
  stroke-linejoin: round; stroke-linecap: round;
}
.profile .sky__fine { stroke-width: calc(var(--sw, .85px) * .2); }
.profile .sky__door { fill: var(--teal); stroke: none; opacity: 1; }
.profile .sky__glow { display: none; }
.band .profile { color: var(--white); }

/* Landscape only: in portrait the title needs the whole width. */
@media (orientation: portrait) {
  .band .profile { display: none; }
}

/* --- the skyline again, on an audience page, above "Who we are" ----------- */

/* Not clickable. It draws itself when scrolled to (site.js). The whole run on
   a wide screen; on a phone, the audience's building and one neighbour each
   side. Each picture is as wide as the page, up to a height cap — beyond that
   it centres and the ground runs on to the edges (it is drawn far past both
   ends, and clipped here).

   It sits inside main, which is held to the reading width, so it breaks out
   to the full width of the window. 100vw counts the scrollbar where there is
   one, so the page clips sideways (clip, not hidden: hidden would make the
   body a scroll container and stop the sticky section headings sticking). */
.audience { overflow-x: clip; }
.strip {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  margin-top: clamp(var(--s6), 7vw, 88px);
  overflow-x: clip;
  pointer-events: none;
}
.strip__svg {
  display: block;
  width: 100%; height: auto;
  max-height: 150px;
  overflow: visible;
}
.strip__svg--three { display: none; }
@media (max-width: 767px) {
  .strip__svg--all   { display: none; }
  .strip__svg--three { display: block; }
}

/* Weights in drawing units from --sw, which site.js sets from the size the
   picture is shown at, for 1.5px on screen (see .profile path for why not
   non-scaling-stroke). The fallback suits a wide screen without JavaScript. */
.strip path {
  fill: none; stroke: #000000; stroke-width: var(--sw, .8px);
  stroke-linejoin: round; stroke-linecap: round;
}
.strip .sky__fine { stroke-width: calc(var(--sw, .8px) * .2); }
.strip .sky__door { fill: var(--teal-d); stroke: none; opacity: 1; }
.strip .sky__glow { display: none; }

/* The section after it keeps its spacing but not its rule: the skyline's
   ground line is the divider. */
main > .strip + section {
  padding-top: clamp(var(--s6), 7vw, 88px);
}

/* ===== the audience page as one scroll ==================================== */

/* Sections are told apart by rules and by type, never by a filled box. */
main > section + section,
main > section + nav {
  margin-top: clamp(var(--s6), 7vw, 88px);
  padding-top: clamp(var(--s6), 7vw, 88px);
  border-top: 1px solid var(--taupe);
}

/* --- what we do: the opening statement ----------------------------------- */

.opening { padding-top: clamp(var(--s6), 7vw, 88px); }
.opening__text {
  max-width: 40ch;
  font-style: italic;
  font-size: clamp(1.2rem, 2.4vw, 1.7rem);
  line-height: 1.6;
  color: var(--slate);
}

/* --- the narrative ------------------------------------------------------- */

.chapter { max-width: 70ch; }
.chapter__lead {
  font-size: clamp(1.2rem, 2vw, 1.5rem);
  line-height: 1.5;
  color: var(--slate);
}
.chapter__body { margin-top: var(--s4); }
.chapter__body p + p { margin-top: var(--s3); }

.sec__head { display: flex; flex-direction: column; gap: 10px; margin-bottom: var(--s4); }
.sec__head .rule { width: 56px; height: 3px; background: var(--teal); }
.sec__head .eyebrow { color: var(--ink-2); }


/* --- two columns: heading and a line on the left, the list on the right --- */

.split {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(var(--s5), 5vw, 72px);
  align-items: start;
}
.split__side { position: sticky; top: var(--s5); }
.split__line {
  max-width: 40ch;
  font-style: italic;
  color: var(--ink-2);
  font-size: 17px; line-height: 26px;
}
.split__line + .split__line { margin-top: var(--s3); }

@media (max-width: 899px) {
  .split { grid-template-columns: 1fr; gap: var(--s4); }
  .split__side { position: static; }
}

/* --- scroll reveal -------------------------------------------------------- */

/* Presentation only. Without JavaScript .is-in is never added, so the rule
   below never applies and everything is simply visible from the start. */
@media (prefers-reduced-motion: no-preference) {
  .js-reveal .reveal {
    opacity: 0;
    transform: translateY(18px);
    transition: opacity .6s ease, transform .6s cubic-bezier(.2, .7, .3, 1);
  }
  .js-reveal .reveal.is-in { opacity: 1; transform: none; }
}

/* --- services: a list, divided by rules, not a grid of cards ------------- */

.svc { max-width: 60ch; }
.svc + .svc {
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid rgba(197, 175, 164, .55);
}
.svc h3 { margin-bottom: var(--s2); }
.svc p { color: var(--ink-2); }

/* About uses the same unit in a grid, where the rule would fall oddly. */
.cards .svc + .svc { margin-top: 0; padding-top: 0; border-top: 0; }
.cards .svc { border-left: 3px solid var(--teal); padding-left: var(--s3); }

/* --- skills and sectors: taupe chevron tags ------------------------------ */

.tags { display: flex; flex-wrap: wrap; gap: var(--s2) 10px; padding: 0; list-style: none; }
.tags li {
  background: var(--taupe);
  color: var(--slate);
  transform: skewX(-12deg);
  padding: 7px 16px;
  font-size: 13px; line-height: 18px;
}
.tags li > span { display: block; transform: skewX(12deg); }
.tags--sectors li { background: var(--slate); color: var(--white); }

/* --- close --------------------------------------------------------------- */

.close {
  margin-top: clamp(var(--s6), 7vw, 88px);
  padding-block: clamp(var(--s6), 7vw, 88px);
  border-top: 1px solid var(--taupe);
}
.close__lead {
  max-width: 54ch;
  font-style: italic;
  font-size: clamp(1.125rem, 1.8vw, 1.35rem);
  line-height: 1.5;
  color: var(--slate);
}
.close__lead + .close__lead { margin-top: var(--s3); }
/* [href] only: ff_email() ships an anchor with no href, which site.js fills in.
   Before that it is text, and it should not be underscored like a link. */
.close__contact a[href] { color: var(--slate); text-decoration: none; border-bottom: 2px solid var(--teal); }
.close__contact a[href]:hover { color: var(--teal-d); }
.close__dot { margin-inline: var(--s3); color: var(--taupe); }

/* --- secondary pages: same band, shallower ------------------------------ */

.band--slim { padding-bottom: var(--s5); }
.band--slim .band__tagline { font-size: clamp(2rem, 4.5vw, 2.75rem); }

/* --- onward routes ------------------------------------------------------ */

.onward {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  margin-top: var(--s6);
  padding-top: var(--s4);
  border-top: 1px solid var(--taupe);
}
.onward__link {
  /* Regular weight and a hairline: these are ways onward, not calls to action. */
  color: var(--slate); text-decoration: none; font-weight: 400;
  border-bottom: 1px solid var(--teal); padding-bottom: 2px;
}
.onward__link:hover { color: var(--teal-d); }

/* --- contact ------------------------------------------------------------ */

.contact-grid {
  display: grid; gap: var(--s6);
  grid-template-columns: minmax(0, 1.6fr) minmax(260px, 1fr);
  align-items: start;
}
@media (max-width: 767px) { .contact-grid { grid-template-columns: 1fr; gap: var(--s5); } }

/* Contact with no form: two lines and an address. Set like the close block at
   the foot of the audience pages, because it is doing the same job. */
.contact-only__lead {
  max-width: 54ch;
  font-style: italic;
  font-size: clamp(1.125rem, 1.8vw, 1.35rem);
  line-height: 1.5;
  color: var(--slate);
}
.contact-only__lead + .contact-only__lead { margin-top: var(--s3); }

.contact-only__email {
  margin-top: var(--s6);
  margin-bottom: var(--s5);
  font-size: clamp(1.35rem, 3vw, 1.75rem);
}
/* The rule under it only appears once site.js has made it a link — an address
   that cannot be clicked should not be dressed as something that can. */
.contact-only__email .mailto[href] {
  color: var(--slate); text-decoration: none;
  border-bottom: 2px solid var(--teal); padding-bottom: 2px;
}
.contact-only__email .mailto[href]:hover { color: var(--teal-d); }

/* Placeholders awaiting real numbers. Deliberately visible. */
.todo {
  background: none;
  box-shadow: inset 0 -2px 0 var(--rose);
  color: var(--ink); padding: 0 2px;
}

/* ------------------------------------------------------------- content --- */

.page { padding-block: var(--s6); }
.prose { max-width: var(--measure); }
.prose p + p, .prose ul + p, .prose p + ul { margin-top: var(--s3); }
.prose h2 { margin-top: var(--s5); margin-bottom: var(--s2); }
.prose h3 { margin-top: var(--s4); margin-bottom: var(--s1); }
.prose ul { padding-left: 1.2em; }
.prose li + li { margin-top: var(--s2); }

/* Rule, not a filled box — nothing on the site differentiates text by shading. */
.panel {
  border-left: 3px solid var(--teal);
  padding-left: var(--s4);
}
.panel + .panel { margin-top: var(--s4); }

/* --- founder -------------------------------------------------------------- */

.founder__grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(var(--s5), 5vw, 72px);
  align-items: start;
}
.founder__pg { font-size: 16px; color: var(--ink-2); letter-spacing: 1px; }
.founder__role { color: var(--ink-2); margin-top: var(--s2); }

/* The file is already a 384px square framed on the head (see FOUNDER_PHOTO in
   inc/content.php), so there is nothing to re-crop here — object-fit is only a
   guard in case the file is ever replaced with something that is not square.
   Shown at 96px: 4x, which holds up on a retina screen. The width/height
   attributes on the <img> reserve the box before it loads; these override. */
.founder__photo {
  width: 96px; height: 96px;
  object-fit: cover;
  border-radius: var(--r-full);
  margin-bottom: var(--s3);
}

/* On the About page it sits in running text rather than in a grid column. */
.founder__photo--float {
  float: right;
  margin: 0 0 var(--s3) var(--s4);
  shape-outside: circle();
}

@media (max-width: 899px) {
  .founder__grid { grid-template-columns: 1fr; gap: var(--s4); }
}

@media (max-width: 479px) {
  /* Too narrow for text to run beside a float without the measure collapsing. */
  .founder__photo--float { float: none; margin: 0 0 var(--s3); }
}

.cards { display: grid; gap: var(--s4); grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }

/* ---------------------------------------------------------------- form --- */

.form { max-width: 34rem; display: grid; gap: var(--s3); }
.field { display: grid; gap: 6px; }
.field > label { font-weight: 600; font-size: 14px; }
.field input, .field textarea, .field select {
  border: 1px solid var(--taupe);
  border-radius: var(--r-sm);
  background: var(--white);
  padding: 11px var(--s3);
}
.field input:focus, .field textarea:focus { border-color: var(--teal); }
.field textarea { min-height: 9rem; resize: vertical; }
.field--check { grid-template-columns: auto 1fr; align-items: start; gap: 10px; }
.field--check input { margin-top: 3px; }
.field--check label { font-weight: 400; font-size: 14px; }
.hp { position: absolute; left: -9999px; }   /* honeypot */

.btn {
  display: inline-block; border: 0; cursor: pointer;
  background: var(--slate); color: var(--white);
  padding: 13px var(--s5); border-radius: var(--r-sm);
  font-weight: 600; text-decoration: none;
  justify-self: start;
}
.btn:hover { background: var(--teal-d); color: var(--white); }

/* Held to the reading measure like .prose, so a callout never runs wider than the
   paragraph above it. box-sizing is border-box globally, so this is the OUTER edge:
   the box lines up with the prose edge and its text sits a padding's width inside,
   which is what an indented callout should do. */
.msg {
  max-width: var(--measure);
  border-radius: var(--r-md);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
}
.msg--ok  { background: #EAF3F2; border-left: 4px solid var(--teal); }
.msg--err { background: #FCEFF0; border-left: 4px solid var(--rose); }
.err { color: #A84B54; font-size: 13px; }

/* ----------------------------------------------------------- resources --- */

.res { display: grid; gap: var(--s4); }
.res__item {
  background: var(--white); border-radius: var(--r-md);
  border-left: 4px solid var(--taupe);
  padding: var(--s4);
  display: grid; gap: var(--s2);
}
.res__meta { color: var(--ink-2); }

/* -------------------------------------------------------------- footer --- */

/* One footer, identical on every page including home. Pale rather than slate,
   with no teal rule across the top: on the home page it meets the warm foot of
   the photograph, and a dark bar with a bright line under it cut the page in
   two exactly where the skyline was meant to settle. */
/* Teal, but a hairline rather than the 4px bar this replaced: on a pale footer
   a thin accent reads as a considered edge, where the heavy version under a
   slate footer cut the page in two right where the skyline settles. Swap to
   var(--taupe) for the quieter option. */
.siteftr { background: var(--pale); color: var(--ink); border-top: 2px solid var(--teal); }
.siteftr__inner {
  padding-block: var(--s5);
  display: grid; gap: var(--s4);
  grid-template-columns: auto 1fr;
  align-items: start;
}
.siteftr__mark { font-size: 22px; letter-spacing: 1px; color: var(--slate); }
.siteftr__mark span { color: var(--teal); }   /* the plus keeps its accent */
/* Two groups, not one flat list: the four doors sit left, next to the mark, and
   the three site pages sit right, with the open space between them doing the
   separating. Neither group wraps internally, so in a narrow window the only
   break is between Directors and About - never mid-set. */
.siteftr__nav {
  display: flex; flex-wrap: wrap; gap: var(--s3) var(--s6);
  justify-content: space-between;
}
.siteftr__doors, .siteftr__pages {
  display: flex; flex-wrap: nowrap; gap: var(--s4); white-space: nowrap;
}
/* Audience pages carry no doors, and space-between would then send the site
   pages to the left. They keep their place on the right. */
.siteftr__pages:only-child { margin-inline-start: auto; }
.siteftr__nav a { color: var(--ink); text-decoration: none; }
.siteftr__nav a:hover { color: var(--teal-d); text-decoration: underline; }
.siteftr__legal {
  grid-column: 1 / -1;
  font-size: 12px; line-height: 18px; color: var(--ink-2);
  border-top: 1px solid var(--taupe); padding-top: var(--s3);
}

/* -------------------------------------------------------------- notice --- */

/* A panel in the bottom right rather than a full-width bar: the bar sat across
   the registered name and the footer links, so the two things a reader might
   actually want at the foot of the page were the two things it covered. It
   stays attached to the bottom edge — rounded at the top corners only — and
   slides down out of view when dismissed. */
.notice {
  position: fixed; bottom: 0; z-index: 50;
  left: auto;
  /* Lines up with the right edge of .wrap, so it reads as part of the page. */
  right: max(var(--s4), (100% - var(--wrap)) / 2);
  /* Wide enough for the sentence to sit on one line, so the panel is a shallow
     strip in the corner rather than a block three lines deep. fit-content takes
     the one-line width where there is room and falls back to wrapping inside
     the max-width when there is not. */
  width: fit-content;
  max-width: min(620px, 100% - 2 * var(--s4));
  background: var(--slate); color: var(--white);
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s2) var(--s4);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: 0 -2px 18px rgba(6, 27, 58, .22);
  font-size: 13px; line-height: 18px;
  transform: translateY(101%);   /* 101% so the shadow clears the edge too */
  transition: transform .3s ease;
}
.notice.is-in { transform: none; }
.notice[hidden] { display: none; }
.notice a { color: var(--white); }
.notice__close {
  margin-left: auto; background: none; border: 0; cursor: pointer;
  color: rgba(255,255,255,.7); font-size: 22px; line-height: 1; padding: 0 6px;
}
.notice__close:hover { color: var(--white); }

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

/* ================================================== VIEW TRANSITIONS ===== */

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root), ::view-transition-new(root) { animation-duration: .28s; }
}

/* ========================================================= RESPONSIVE ===== */

/* Skyline layout is switched in JS by page width, at 1024 and 600.
   These rules cover the surrounding composition only. */

@media (max-width: 1023px) {
  .band__inner { grid-template-columns: 1fr; align-items: start; }
  .home__statement { padding-block: var(--s5); }
}

@media (max-width: 767px) {
  /* Back link stays on the logo's row, pushed to the right. */
  .masthead { align-items: center; gap: var(--s3); }
  .backlink { text-align: right; }
  /* Just "← home" on phones. Clipped rather than display:none, so screen
     readers still hear "Back to home". */
  .backlink__short { display: inline; }
  .backlink__lead, .backlink__full {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(0 0 0 0); white-space: nowrap;
  }
  /* Seven links will not share a row with the mark, so on a phone the nav takes
     a full-width line of its own and the mark drops below it. The two groups
     stay separate spans, so when the line does run out the break falls between
     Directors and About rather than mid-set. */
  .siteftr__inner {
    grid-template-columns: 1fr;
    padding-block: var(--s4); gap: var(--s3);
  }
  .siteftr__mark { font-size: 18px; }
  .siteftr__nav {
    order: -1;                    /* above the mark, though it follows in source */
    gap: var(--s2) var(--s5); font-size: 12px;
  }
  .siteftr__doors, .siteftr__pages { gap: var(--s3); }
  .siteftr__pages:only-child { margin-inline-start: 0; }

  /* On a phone the panel widens and centres, still attached to the bottom edge,
     and takes the height of the registered-name line it sits over. site.js
     measures that line and writes --notice-h; min-height rather than height so
     a longer wrap grows the panel instead of clipping the sentence. */
  .notice {
    inset-inline: 0; margin-inline: auto;
    width: min(420px, 100% - 2 * var(--s4));
    min-height: var(--notice-h, auto);
    align-items: center;
    padding: var(--s2) var(--s3);
    font-size: 12px; line-height: 16px;   /* caption size: two lines, not three */
  }


  /* .home keeps min-height: 100svh and .home__main keeps 1fr auto here. An
     earlier override dropped both on phones, which detached the footer from
     the bottom of the screen whenever the content was shorter than the
     viewport - which, since the skyline became three short rows, it usually
     is. The footer is a footer at every width. */
}

@media (max-width: 599px) {
  :root { --s6: 36px; --s5: 24px; }
  .wrap, .masthead, .band__inner, .home__statement {
    width: calc(100% - 2 * var(--s3));
  }
  .tags li { font-size: 12px; padding: 6px 13px; }
  /* Tighter gaps buy the seven links another phone width before the nav has to
     break between the doors and the pages. */
  .siteftr__nav { gap: var(--s2) var(--s4); }
  .siteftr__doors, .siteftr__pages { gap: var(--s2); }
}

/* --------------------------------------------------- release changelog --- */
/* The table on a download page showing what has changed since the version the
   visitor is running. The only table on the site, so these are the only table
   styles: kept local to .changes rather than set on bare `table`, so adding a
   second, different table later does not inherit decisions made for this one. */

.changes {
  width: 100%;
  border-collapse: collapse;
  margin-top: var(--s3);
  font-size: 15px;
  line-height: 22px;
}

.changes th,
.changes td {
  text-align: left;
  vertical-align: top;
  padding: var(--s3) var(--s2);
  border-bottom: 1px solid var(--grey);
}

/* The header row is the only place the taupe earns its keep: it is a background
   token and never a text one. */
.changes thead th {
  font-size: 12px;
  line-height: 16px;
  font-weight: 600;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  border-bottom: 2px solid var(--slate);
  white-space: nowrap;
}

/* Version numbers are row headers, not data: they identify the row. */
.changes tbody th {
  font-weight: 600;
  white-space: nowrap;
  width: 1%;
}

.changes tbody th .caption {
  display: block;
  font-weight: 400;
  color: var(--teal-d);
  text-transform: uppercase;
  letter-spacing: 1.5px;
  font-size: 12px;
}

/* The visitor's own version: the baseline row, deliberately quieter than the
   releases above it, and marked by more than colour alone - it also says "yours"
   and carries a rule. */
.changes__yours {
  background: var(--grey);
}

.changes__yours th,
.changes__yours td {
  border-bottom: none;
}

.changes__yours th .caption { color: var(--slate); }

/* The four-column variant on the confirmation page: File · Version · Released · Notes.
   The file cell is empty on every row but the first, which is deliberate - we serve the
   current release only, and a link there would imply we can hand back an old one.
   Set in caption size throughout, so the body cells match the headers and the
   captions under the file name and version rather than sitting a size above them. */
.changes--files {
  font-size: 12px;
  line-height: 16px;
}

.changes--files tbody td:first-child a { font-weight: 600; }

.changes--files tbody td:first-child .caption,
.changes--files tbody th:first-of-type .caption {
  display: block;
  color: var(--ink-2);
  text-transform: none;
  letter-spacing: 0;
}

/* The version column is a row header in both variants, so it is not always the first
   cell - in the four-column table it follows the file. Width kept off the file column
   so the link has room. */
.changes--files tbody th { width: auto; }
.changes--files td:first-child,
.changes--files th:first-child { width: 30%; }

@media (max-width: 599px) {
  /* Dropping a column rather than scrolling sideways: the release date is the one a
     reader on a phone is least likely to have come for.

     Note this removes it for EVERYONE at this width, screen readers included -
     display:none is not a visual-only hide. That is accepted rather than worked
     around: the date is context, the version and the change are the content, and a
     visually-hidden column read aloud with no way to see it is worse than an honest
     narrower table.

     The two variants drop DIFFERENT children because the date sits in a different
     place in each: second of three in the plain table, third of four once the file
     column is in front of it. The :not() keeps the plain rule from firing on the
     wider table and hiding its Version column instead. */
  .changes:not(.changes--files) th:nth-child(2),
  .changes:not(.changes--files) td:nth-child(2) { display: none; }

  .changes--files th:nth-child(3),
  .changes--files td:nth-child(3) { display: none; }

  .changes--files td:first-child,
  .changes--files th:first-child { width: 40%; }

  .changes th,
  .changes td { padding: var(--s2) var(--s1); }
}


@media print {
  .notice, .skyline, .strip, .backlink, .skip, .me-flag { display: none !important; }
  body { background: #fff; }
  .band { background: none; color: #000; }
  .band__tagline, .band__eyebrow, .band__sub { color: #000; }

  /* Print the whole argument, not whatever happened to be open on screen.
     site.js opens every <details> on beforeprint; this just hides the controls. */
  summary { display: none; }
  .svc, .opening__text, .chapter__lead { break-inside: avoid; }
}
