/* ═══════════════════════════════════════════════════════════════
   Kitto — styles.css  (restrained rebuild)
   Brand: #141414 black · #FFED14 yellow · paper #FAF9F4 · grey
   Type:  DIN 2014 Regular, licensed from ParaType and served from assets/fonts.
═══════════════════════════════════════════════════════════════ */

/* ═══════════════ TYPEFACE ═══════════════
   DIN 2014 Regular, ParaType. Web licence purchased 20 Aug 2026, so unlike the Galvji
   arrangement it replaces, this font is ours to serve from our own domain.

   What it replaces: an @font-face with src: local('Galvji') and no url(), which gave Macs
   Apple's Galvji and everybody else the platform UI font. The site therefore rendered in a
   different typeface depending on the visitor's machine. It now renders in DIN 2014
   everywhere, which is the whole point of buying the licence.

   ONE WEIGHT ONLY. The licence covers Regular, and the site has never asked for anything
   else: every font-weight in this file is 400. Do not introduce a bold rule without also
   buying and adding the matching file, or the browser will synthesise a smeared fake bold.

   The stack below is the loading fallback, seen for a few hundred ms on a cold cache and on
   the rare request where the font 404s. font-display: swap keeps text visible throughout. */
@font-face {
  font-family: 'DIN 2014';
  src: url('assets/fonts/din-2014_regular.woff2') format('woff2'),
       url('assets/fonts/din-2014_regular.woff') format('woff');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ─── Reset ──────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; border: none; }

:root {
  --ink:      #141414;
  --white:    #ffffff;
  --paper:    #faf9f4;
  --yellow:   #ffed14;
  /* Kitto panel grey. FINAL BRAND GREY #89949b, confirmed by Cam 26 Aug 2026. This is the
     brand value, not a working approximation: it supersedes the #8a949b set on 24 Aug and
     should not be nudged again without him saying so. One step darker in red only, so every
     contrast figure below stands as written.
     ⚠️ YELLOW SITS AT 2.56:1 ON THIS, against 4.50 for normal text and 3.00 for 24px and up.
     Nothing yellow clears either threshold here, not even the display type.
     News article body copy is unaffected, it went white-with-ink on 24 Aug. But every piece
     of yellow that REMAINS is small, and all of it is on this grey:
       site nav, Hire / News / About      15px
       page-head breadcrumbs and meta     13-15px
       basket panel item names, Checkout  16-18px
       kit modal text
     The logo and the page-head h1 are big enough to read as shapes rather than as type,
     which is why the page can look plausible at a glance while the working parts do not.
     Ink is what this grey actually pairs with: 5.96:1. If yellow has to stay in the chrome,
     the grey needs to sit at or below roughly #696969.
     Everything tried today, with its yellow contrast: #383e43 RAL 7016 (8.96), #636363
     (4.97, the long-standing previous value), #67696f (4.54), #616a7c (4.50), #646d80
     (4.30), #7c7f88 (3.31), #888b95 (2.81), #89949b (2.56, current, FINAL), #9a9da6 (2.24).
     Dead end, do not retry: deepening the yellow makes this worse, not better. Yellow is the
     lighter of the pair, so a deeper yellow closes the gap rather than opening it. */
  --grey:     #89949b;
  /* Light text for DARK surfaces: the black kit dropdown, the dark form block. Renamed
     from --gray on 10 Sep 2026. It sat one letter from --grey, the #89949b brand panel
     colour, while being an unrelated near-white, so writing the wrong one gave a
     plausible but wrong result in either direction. Named for what it is for. */
  --on-dark:  #ededed;
  --line:     #e2e1da;
  --mid-grey: #7e7e7e;

  --font: 'DIN 2014', 'Helvetica Neue', Arial, sans-serif;

  /* ── one consistent type scale, used everywhere ──
     1 display · 2 statement · 3 title · 4 lead · 5 body · 6 small · 7 xs */
  /* The two fluid steps share one ratio (4:3) and one viewport range (480→1200px), so the
     gap between them is identical at every width. Previously they grew on different vw
     coefficients and finished at different widths, so the interval drifted mid-range. */
  --fs-display:   clamp(2.25rem, 1.75rem + 1.667vw, 3rem);        /* 36 → 48px */
  --fs-statement: clamp(1.6875rem, 1.3125rem + 1.25vw, 2.25rem);  /* 27 → 36px */
  --fs-title:    1.5rem;                       /* section + card titles, equipment header */
  /* THE TEXT END OF THE SCALE MOVED UP ONE STEP WHEN THE FACE CHANGED, 20 Aug 2026.
     Galvji's x-height is 0.558em, DIN 2014's is 0.500em, so at an identical font-size DIN
     renders 10.4% smaller to the eye. Cap height only drops 5% (0.726 to 0.690), which is
     why the change is felt in body copy and barely at all in headings, and why only the
     bottom of the scale moved. 20px is the exact x-height match to Galvji at 18px.
     `--fs-lead` went 20 to 22 with it: leaving it at 20 would have collapsed it into
     `--fs-body` and cost the scale a step. `--fs-15` and `--fs-13` are deliberately NOT
     bumped, they set uppercase meta where cap height rules and the 5% is not visible, and
     their names would stop being true. Revisit if mixed-case small text starts to read short. */
  --fs-lead:     1.375rem;                     /* 22px, was 20 under Galvji */
  /* TRIAL, 23 Aug 2026: 20px -> 18px on Cam's call. This walks back the x-height
     compensation described above, so DIN at 18 reads about where Galvji sat at 16. Revert
     by putting this back to 1.25rem; nothing else needs touching. If it stays, the comment
     above has to be rewritten, because it currently argues for the 20 this replaces. */
  --fs-body:     1.125rem;                     /* 18px, trial down from 20 */
  --fs-15:       0.9375rem;
  --fs-13:       0.8125rem;
  /* 12px, added 10 Sep 2026. Not a new size: it was already set 19 times as a bare `12px`,
     on prices, breadcrumbs, card dates, cookie text, checkout notes and disclaimers. The
     scale has been an 8-step scale in practice for some time and this only says so. Named
     by px like its two neighbours. Anything smaller than this is too small to set here. */
  --fs-12:       0.75rem;
  /* SIX font-sizes in this file are deliberately NOT on the scale, checked 10 Sep 2026.
     Four are glyph buttons, where the number sets how big a × or an arrow reads inside its
     hit area, not how big any type is: .kit-search-clear 1.6rem, .basket-close 1.9rem,
     .co-remove 1.4rem, .slider-arrow 1.3rem. All four sit on --lh-flat, which is the tell.
     Rounding them to a type step would change the optical weight of the control for no
     reason. The fifth is `input, textarea, select` at 1rem, which is the iOS zoom guard,
     see the note on that rule. The sixth is .hero-statement at 3.75rem (60px), a one-off
     display size above --fs-display's 48px ceiling.
     Everything else goes through a token. If a bare font-size appears below, it is drift. */

  /* ── tracking, one value per size band ──
     DIN 2014 sets narrow: its `n` advance is 82% of Galvji's, so at the same tracking the
     words close up. Set on `body` so text inherits it, then overridden on the large classes
     below. NOT a kerning fix: DIN carries real kern pairs (Yo, Ta, P. and so on) and Galvji
     carries none at all, so pair fit was never the problem here.
     THE FACE DOES NOT WANT THE USUAL OPTICAL CURVE. The received rule is to open at text
     sizes and tighten to negative at display sizes, because the same letter-spacing reads
     looser the bigger it gets. That rule assumes a face with normal sidebearings and something
     to give up. DIN has neither: it is already narrow, and the large sizes here are all long
     enough to be read rather than grabbed. Tested at 48px on 23 Aug across -.015, 0, .01 and
     .02, and .02 was the one that held. */
  --track-body:    .02em;    /* 20px body copy, chosen off the ladder */
  --track-small:   .03em;    /* 13-15px, small text always wants more */
  --track-title:  -.005em;   /* 22-24px, just off zero */
  /* 36-60px: the about lead, the intro statement and the home hero, all on one value.
     Was -.015em until 23 Aug, when the about lead showed what that setting costs: at 48px
     "London-based kit hire" closed up and the tt in Kitto nearly touched. The hero had
     already been tracked out to .02em by eye on 20 Aug and had its own token as the
     exception. It is not the exception, it was the only one set correctly, so the exception
     token is gone and the whole band sits here. */
  --track-display: .02em;
  /* LEADING FOR NARROW COLUMNS. `p` is set once at 1.5, which is right for body copy at a
     full measure and wrong everywhere the column is a third of that. Leading exists to walk
     the eye back to the start of the next line, so it has to track measure: 45-75 characters
     wants 1.4-1.6, under 40 wants 1.2-1.35. Measured 19 Aug, the partnership paragraph, the
     quote intro and the three value blocks all run 23-30 characters a line in columns of
     276-352px, and all three had inherited 1.5. Gill Sans pushes the same way, its modest
     x-height against long ascenders and descenders already separates the lines.
     1.35 rather than 1.25 because these columns are not one width: the value blocks are
     276px and about 24 characters at 1213, but stack to 430px and about 45 characters below
     900, and the quote intro runs to 41. One number has to sit right across 24-45, which is
     the top of the short-measure range, not the bottom. 1.25 was tested and reads well at
     24 but starts to close up by 45.
     Applied by hand rather than by a rule, because the measure is a property of the column
     the text lands in, not of the text. Anything set in a narrow column wants this. */
  --lh-narrow:   1.3;
  /* LEADING FOR BODY TEXT AT A FULL MEASURE, the other half of the pair. 1.45 was already
     the de facto value here, .article-body p and .page-head .sub both used it, while the
     global `p` sat on 1.5 and nothing else did. Naming the two and using only these two is
     the whole point: before this the site set body-size text on 1.35, 1.4, 1.45, 1.5, 1.6
     and 1.7 with nothing distinguishing them.
     NARROW OR BODY IS DECIDED BY MEASURE, NOT BY IMPORTANCE. Under about 45 characters a
     line, use --lh-narrow. Over, use --lh-body. Captions, labels, credits and the legal
     pages are not body text and keep their own values. */
  --lh-body:     1.4;
  /* THE THREE LEADINGS THAT WERE NEVER NAMED, added 10 Sep 2026. --lh-body and --lh-narrow
     cover running text, and the pair was introduced to stop the site setting body copy on
     six different numbers. It worked, but it only ever covered running text, so 34 rules
     that are not running text sat on bare values instead: 1 (18), 1.15 (9) and 1.2 (7).
     These are not a measure decision, they are the leadings for text that is a shape or a
     control rather than something you read across: buttons, counters, single-line labels,
     headings. Naming them closes the last gap where a bare number was the normal thing to
     write, which is how the one-off tail (1.04, 1.06, 1.1, 1.14, 1.25, 1.45, 1.5, 1.55)
     crept back in after the first pass. Fold those into the nearest of these five. */
  --lh-flat:     1;      /* buttons, counters, icon rows: the box sets the height */
  --lh-head:     1.15;   /* headings, where the eye takes the line as a whole */
  --lh-tight:    1.2;    /* short stacked lines, card titles, two-line labels */

  /* aliases (kept so existing rules resolve to the scale) */
  --fs-hero:   var(--fs-display);
  /* Interior page titles sit one step DOWN, on statement. Display is now reserved for the
     about lead, which is meant to read as exaggerated — it could not do that while every
     page title was the same size. */
  --fs-banner: var(--fs-statement);
  --fs-big:    var(--fs-statement);
  --fs-25:     var(--fs-title);

  /* spacing */
  --gut: 80px;
  --sec: clamp(56px, 8vw, 110px);
  --max: 1320px;
  --maxwide: 2700px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* LEADING FOR THE YELLOW CREDIT BLOCKS, the hero tiles and the pictures in a journal
     article. One number for both, because they were set a different way in each place and
     drifted: the tiles were 1.25 with a 1px gap between the lines, about 17px, while the
     journal blocks set nothing and inherited a 16px/1.5 strut from the article, landing on
     24px. Neither number was chosen.
     This is the tiles' setting, which was the right one: 13px x 1.33 = 17.3px, the same
     17px the tiles have always shown, now stated outright instead of coming half from a
     line-height and half from a flex gap. The journal blocks were tried at their own 24px
     across both pages and it read too open, a label with the lines drifting apart rather
     than a credit block. Tight is correct here; the block is small, and what holds it
     together is the lines staying near each other.
     TIGHTER STILL (Cam, 10 Sep: "the image captions overlay are too leaded on the text
     breaks, they need to sit closer"). 1.33 is fine on the one-liners, which is most of
     them, and it only shows when a credit wraps: at 13px that was 17.3px of line, and two
     or three wrapped lines of a credit read as separate statements stacked up rather than
     as one block of small type. 1.18 is 15.3px, which closes the gap without the descenders
     of one line meeting the caps of the next.
     ONE TOKEN, DELIBERATELY. Every overlaid caption on the site reads this, the tiles, the
     footer carousel, the expanded view and the journal pictures, so they stay set the same
     way. The expanded view's credit and its Close also line up only while both are on this
     value, which is why they both take the token rather than a number. */
  /* Loosened with the tile block on 10 Sep, 1.18 to 1.24. The two moved together at every
     step: they are the same caption in two arrangements, and a gap between them reads as one
     of them being wrong. */
  --cap-lh: 1.24;
}

/* ═══════════════════════════════════════════════════════════════════════════════════════
   BREAKPOINTS: A SPINE, PLUS CONTENT BREAKS THAT ARE MEANT TO BE ODD
   ═══════════════════════════════════════════════════════════════════════════════════════
   Audited 10 Sep 2026. The file uses 23 distinct widths, which reads as drift until you
   look at what each one does. It is two things, and only the first is a system.

   THE SPINE, for anything about page structure. Gutters, columns, the nav:
       576 / 768 / 992 / 1200,  with min-width partners at 577 / 769 / 993 / 1201.
   768 is the phone boundary and carries 25 blocks. If you are writing a layout rule and
   have no specific reason to do otherwise, use these.

   CONTENT BREAKS, where a particular element stops fitting at a particular width. These
   are deliberately not round numbers, because the number is a measurement of the content,
   not a device:
       400   the logo stops centring in the header
       430   the cookie buttons stop fitting on one row
       463   the eight partnership logos drop from three columns to two
       480   the slider caption measure
       599   the partnership block stacks (pairs with the 600 min-width above it)
       700   the slider controls leave the top edge
       900   the three home category tiles stop working across (pairs with 577 min)
       1100  the hero tile caption
       1150  the partnership gutter reopens, Cam's call 19 Aug
   Do NOT round these to the spine. 463 exists because that is where three columns of eight
   differently-sized logos stop fitting; 576 would break the thing it was written to fix,
   and 900 forced to 992 would narrow the category tiles for no reason across 92px.

   Fixed on 10 Sep: two blocks used max-width 767px against 23 at 768px, so at exactly
   768px, iPad portrait, .form-inner-wrap and .form-block .form-grid stayed two-column
   after every other mobile rule had fired. Both are 768 now. If you add a max-width and a
   min-width pair, they must be N and N+1 or you leave a one-pixel hole like that one.
   ═══════════════════════════════════════════════════════════════════════════════════════ */

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

body {
  font-family: var(--font);
  color: var(--ink);
  background: var(--yellow);
  /* The inherited default for everything that never states a leading of its own. It sat on
     1.5, above even the old --lh-body, so anything the tokens did not reach was the most
     openly set text on the site. It reads the token now, so tightening the pair tightens
     the page rather than only the elements that were already doing it properly. */
  line-height: var(--lh-body);
  /* THE BASE SIZE BELONGS HERE, and it has to be here for the tracking below to be right.
     `letter-spacing` in em resolves against the element it is DECLARED on, then inherits as
     a fixed length. With body left at the 16px default, .02em computed to 0.32px and reached
     20px body copy as .016em, not the .02em that was chosen. Declaring the size makes .02em
     resolve at 20px, so it lands as 0.4px, which is the value that was actually picked. */
  font-size: var(--fs-body);
  letter-spacing: var(--track-body);

  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}
/* About comes off this list: its page is yellow, like the home page. The KitMapper block at
   the foot of it keeps its own white ground, exactly as it does on the home page, so the two
   read as the same block on the same page rather than the same block on two grounds. */
body.bg-white, body[data-page="equipment"], body[data-page="newsletter"] { background: var(--white); }

/* fade into each page + the catalogue view */
@keyframes pageIn { from { opacity: 0; } to { opacity: 1; } }
/* ONE ARRIVAL, NOT TWO. main used to fade itself in over .38s as well, underneath the yellow
   overlay that was fading out over .5s, or 1.15s on the home page. Two opacity curves running
   over the same content at once, at different speeds, is what made the load read as a couple
   of flashes before the page settled: the content came up, the yellow was still going, and
   the two crossed. The overlay is the arrival now and it is the only thing moving. */
html { background-color: var(--yellow); }   /* yellow shows through during page fades */
/* NO RUBBER-BAND ON THE DOCUMENT (Cam, 10 Sep: "I can scroll past the image and I see
   yellow", and "if I scroll up on top of the page I also see yellow which needs to not
   happen").
   The yellow above is deliberate and stays: it is what shows through while a page fades, and
   .page-fade right below is the element that relies on it. The fault is not the colour, it is
   that the macOS bounce EXPOSES it. Overscrolling past the foot of a page pulls the document
   up off its own background, and past the top pulls it down, and in both cases what appears
   in the gap is the root colour, so the brand yellow flashes at either end of every page.
   overscroll-behavior-y stops the bounce without touching the colour, so the fade still has
   its ground and the reader never sees it.
   THE TRADE: this also disables pull-to-refresh in mobile browsers, which is the same
   gesture. That is the accepted cost of the yellow never appearing, and it is worth knowing
   before anyone wonders where pull-to-refresh went.
   The catalogue panel sets the same thing on .catalogue-locked further down the file, which
   this now makes redundant at the document level; it is kept there because that rule also
   carries the overflow lock and reads as one statement about the panel owning the screen. */
html { overscroll-behavior-y: none; }
/* ONE SYMMETRIC CURVE, BOTH DIRECTIONS (Cam, 10 Sep: "I get the fade out but the fade in
   doesn't match", then "it just feels like a dash rather than a smooth transition").
   Both directions used var(--ease), cubic-bezier(0.22, 1, 0.36, 1), a quint ease-OUT. On the
   REVEAL that is right. On the COVER the steepness is at the wrong end: the yellow slams to
   nearly solid in the first fraction and crawls through the last sliver, so leaving read as a
   flash where arriving read as a fade.
   Mirroring it, cubic-bezier(0.64, 0, 0.78, 0), matched them mathematically and was worse to
   watch: that is a quint ease-IN, almost nothing for half the duration and then a rush. A
   correct mirror, and the dash.
   The answer is not to mirror an aggressive curve but to stop using one here. This bezier is
   its OWN mirror, symmetric about the centre, so cover and reveal are the same motion by
   construction rather than by a pair of values that have to be kept in step. It is also far
   gentler than a quint at both ends, which is what a full-screen colour wants: it should
   arrive and leave without ever appearing to accelerate.
   --ease is untouched and still does its job everywhere else. It is the right curve for
   something that moves and settles; a fade neither moves nor settles. */
/* QUICK (Cam, 10 Sep: "should be a quick fade in yellow, fade out yellow into the page").
   .5s each way meant a full second of yellow between one page and the next, which is a long
   time to look at a colour on a click you have already committed to. .32s reads as a wipe of
   colour rather than as a wait, and the symmetry above is what lets it stay legible that
   short: with no acceleration at either end there is no moment the eye has to catch. */
.page-fade { position: fixed; inset: 0; z-index: 200; background: var(--yellow); opacity: 1; pointer-events: none; transition: opacity .32s cubic-bezier(0.45, 0, 0.55, 1); }
.page-fade.hide { opacity: 0; }
/* Slower on arrival at the home page, so the hero comes up rather than snapping in. Only the
   .hide direction, because the cover is what a click waits on.
   SCALED DOWN WITH THE REST, 1.15s to .6s. The original was set against a .5s base; left
   alone beside a .32s one it would be nearly four times the site's own fade and read as the
   home page hanging. Still visibly the slowest arrival on the site, which was the point.
   (The 480ms navigate timeout that note refers to is gone: main.js waits on transitionend
   now, with a timer only as a backstop, so this duration and that one cannot drift apart.) */
body.home .page-fade.hide { transition-duration: .6s; }
@media (prefers-reduced-motion: reduce) { .page-fade { display: none !important; } }
/* #kitCatalogue is deliberately NOT in here. It takes catalogueDrop further down, the same
   animation the home page's catalogue uses, and this rule was quietly winning over it:
   `:not([hidden])` carries an attribute selector, so it out-specifies the plain #kitCatalogue
   below and the equipment page opened with a fade while the home page dropped. */
#kitResults:not([hidden]) { animation: pageIn .32s var(--ease); }
@media (prefers-reduced-motion: reduce) { main, #kitCatalogue, #kitResults { animation: none; } }

/* fade-in on scroll (JS adds .reveal, then .in when visible) */
.reveal { opacity: 0; transform: translateY(14px); transition: opacity .6s var(--ease), transform .6s var(--ease); }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .reveal, .reveal.in { opacity: 1; transform: none; transition: none; } }

aside, nav, footer, header, section, main, article { display: block; }
h1, h2, h3, h4, h5, h6, p { font-size: inherit; font-weight: inherit; }
ul, ul li { list-style: none; }
img, svg { max-width: 100%; height: auto; display: block; vertical-align: top; }
address { font-style: normal; }
button { cursor: pointer; background: none; }
button:focus-visible, a:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
/* Name the stack rather than inheriting it. iOS does not reliably pass the inherited
   family down into form controls, so the buttons were rendering in the system UI font
   while everything around them used the site stack. */
/* Name the stack rather than inheriting it, AND reset appearance. iOS Safari renders a
   <button> in the system UI font, with its own rounding, whatever font-family says, until
   the native appearance is turned off. That is why "Send enquiry" kept looking wrong on a
   phone while matching everything around it on desktop. */
input, textarea, button, select {
  font-family: var(--font); font-size: inherit; color: inherit; background: transparent;
  -webkit-appearance: none; appearance: none; border-radius: 0;
}
/* select keeps its own arrow, drawn in CSS, so appearance:none is wanted there too */
label { cursor: pointer; }

a { color: var(--ink); text-decoration: none; }

/* underline on hover only — hugs the text itself, not the whole block */
.u, .u-rev { text-decoration: none; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.u:hover, .u-rev:hover { text-decoration: underline; }

.color-grey, .color-grey a { color: var(--mid-grey); }

/* hierarchy reduction — drop the grey small-caps overlines entirely */
.eyebrow { display: none; }

/* reveal disabled — no scroll animation */
.reveal { opacity: 1; transform: none; }
/* ...EXCEPT THE JOURNAL CARDS (Cam, 19 Aug: "can we fix animation").
   The rule above turns the whole scroll-reveal system off site-wide, and it sits 37 lines
   BELOW the one that defines it, so .reveal resolved to opacity 1 and nothing ever faded.
   The staggered delays on the cards were being computed correctly the whole time and then
   applied to a transition with nothing to travel.
   Re-enabled here for the cards alone rather than by deleting the line above: that switch
   was thrown deliberately, and the load-in that was actually asked for is this one. Two
   classes against one, so these win without touching it. */
/* Used for a single frame while the start state is applied, so the drop to opacity 0 is
   not itself animated. See the reveal block in main.js. */
.reveal-instant { transition: none !important; }
.news-card.reveal { opacity: 0; transform: translateY(14px); }
.news-card.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .news-card.reveal, .news-card.reveal.in { opacity: 1; transform: none; transition: none; }
}

/* ─── Typography helpers ─────────────────────────────────────── */
.header-1 { font-size: var(--fs-25); line-height: var(--lh-tight); }
.header-2 { font-size: var(--fs-15); }
.lead  { font-size: var(--fs-lead); line-height: var(--lh-body); }
p { font-size: var(--fs-body); line-height: var(--lh-body); }
.kit-small { font-size: var(--fs-15); line-height: var(--lh-body); display: block; }

/* TRACKING BY SIZE BAND. `body` sets the text value and everything inherits it, so anything
   set larger has to take it back or it arrives at 36px still carrying 20px's tracking, which
   reads slack. Grouped here rather than added to each rule so the curve is legible in one
   place and a new large element only has to join the list. The class list mirrors the type
   scale: whatever uses --fs-title and up belongs here. */
.header-1,
.lead,
.mobile-menu a,
.home-cat-body .name { letter-spacing: var(--track-title); }

.intro-statement,
.about-lead p,
.about-contact p { letter-spacing: var(--track-display); }

/* Small text goes the other way, it wants more than body, not less. */
.header-2,
.kit-small { letter-spacing: var(--track-small); }

/* ─── Layout ─────────────────────────────────────────────────── */
.container-wrapper { max-width: var(--maxwide); width: 100%; margin: 0 auto; }
.container {
  width: 100%; max-width: var(--max);
  padding-inline: var(--gut); margin-inline: auto;
}
main > section { padding-block: var(--sec); }

.section-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 2rem; margin-bottom: clamp(1.8rem, 3vw, 2.8rem); flex-wrap: wrap;
}
.section-head .header-1 { line-height: var(--lh-head); }
.line { width: 1px; align-self: stretch; background: var(--line); }
.rule { height: 1px; background: var(--line); border: 0; }

/* full-width media (breaks out of container) */
.media-full { padding-block: 0 !important; }
.ph {
  width: 100%; background: var(--paper); border: 2px solid var(--line);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  color: var(--mid-grey); font-size: var(--fs-13); letter-spacing: .02em; text-align: center;
}
.ph img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ar-21-9 { aspect-ratio: 21 / 9; }
.ar-16-9 { aspect-ratio: 16 / 9; }
.ar-14-9 { aspect-ratio: 14 / 9; }
.ar-3-2  { aspect-ratio: 3 / 2; }
.ar-4-3  { aspect-ratio: 4 / 3; }
.ar-4-5  { aspect-ratio: 4 / 5; }
.ar-10-11{ aspect-ratio: 10 / 11; }

/* real images inside the modules */
.slider-media img { width: 100%; aspect-ratio: 14/9; object-fit: cover; display: block; }
.selections-item-img-wrap img { width: 100%; aspect-ratio: 5/4; object-fit: contain; }
.news-card-media, .news-feature-media { overflow: hidden; display: block; }
.news-card-media img, .news-feature-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* newsletter image hover — yellow 50% overlay (home block + listing page) */
.news-card .ph, .news-card .news-card-media { position: relative; overflow: hidden; }
/* A very slight lift under the thumbnails. Kept low and soft: these sit on white, so
   anything heavier reads as a border rather than depth. */
/* Same lift AND the same yellow stroke as the hero tiles, so a picture behaves the same
   way wherever it appears on the site. outline with a negative offset draws the stroke
   inside the frame, so it costs the grid no height. */
.news-card .ph, .news-card .news-card-media {
  /* SHADOW REMOVED (Cam, 24 Aug): was `box-shadow: 0 6px 22px rgba(0,0,0,.34)`. */
  outline: 2px solid var(--yellow); outline-offset: -2px;
}
.news-card .ph::after, .news-card .news-card-media::after { content: ""; position: absolute; inset: 0; background: var(--yellow); opacity: 0; transition: opacity .25s var(--ease); pointer-events: none; z-index: 1; }
.news-card .ph:hover::after, .news-card .news-card-media:hover::after { opacity: .5; }
/* The hero tiles' pixel screen, over the same yellow wash, so a picture behaves the same
   way wherever it is on the site. Same 6px cell: it is the cell size that makes this read
   as the image going coarse rather than as a grid laid on top of it. */
.news-card .ph::before, .news-card .news-card-media::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    repeating-conic-gradient(rgba(0,0,0,.16) 0% 25%, rgba(255,255,255,.10) 0% 50%)
    0 0 / 6px 6px;
  opacity: 0; transition: opacity .25s var(--ease);
}
.news-card .ph:hover::before, .news-card .news-card-media:hover::before { opacity: 1; }
/* Category flag, top right of the thumbnail. z-index 2 so it stays legible over the
   yellow hover wash, which sits at z-index 1. */
.news-cat {
  position: absolute; top: 0; right: 0; z-index: 2;
  background: var(--yellow); color: #000;
  font-size: var(--fs-13); line-height: var(--lh-flat); letter-spacing: .01em;
  padding: 7px 10px;
}
.full-img { width: 100%; aspect-ratio: 21/9; object-fit: cover; display: block; }

/* ─── Buttons ────────────────────────────────────────────────── */
.button {
  display: inline-flex; align-items: center; justify-content: center; gap: .6em;
  background: var(--ink); color: #fff; border: 2px solid var(--ink);
  padding: 12px 24px; font-size: var(--fs-body); line-height: var(--lh-flat); text-align: center;
  transition: background-color .3s var(--ease), color .3s var(--ease);
}
.button { font-family: var(--font); -webkit-appearance: none; appearance: none; border-radius: 0; }
.button:hover { background: transparent; color: var(--ink); border-color: var(--ink); }
.button.block { width: 100%; }
.button.sm { font-size: var(--fs-15); padding: 11px 20px; }
/* Two steps below .sm, which is 16px and shared with the modal's Add to basket. Set on
   --fs-13, the same token as the image credits on the hero tiles and in the journal, so a
   control sitting beside a heading reads at caption weight rather than competing with it.
   Padding pulled in to match. */
.button.xs { font-size: var(--fs-13); padding: 7px 13px; }
.button.ghost { background: transparent; color: var(--ink); }
.button.ghost:hover { background: var(--ink); color: #fff; }
/* Opts out of the full-width buttons on a phone: two classes, so it beats the plain
   `.button { width: 100% }` in the 576 block wherever that sits in the file. */
.button.auto { width: auto; }
/* Wrapper for a lone call to action under a block. Centred on a phone, where a
   content-width button left on its own edge reads as unfinished rather than placed. */
.cta-row { margin-top: 3rem; }
@media (max-width: 576px) { .cta-row { text-align: center; } }

/* ═══════════════════ HEADER ═══════════════════ */
/* nav floats over the content (transparent), black elements */
header.site { background: transparent; position: fixed; top: 0; left: 0; right: 0; z-index: 50; pointer-events: none; }
/* fixed top nav: clear it on content pages (home hero is full-bleed under the bar) */
/* Clearance for the fixed bar. --header-h is measured from the real header in main.js
   and kept in step on resize; the 72px is only the value used before that runs.
   It used to drop to 62px under 992px, but nothing actually makes the header shorter
   on mobile, so the first line of every page sat 10px under the bar. */
/* Clearance for the fixed bar, and it must not depend on JavaScript. --header-h is set
   by main.js only AFTER the header is injected and measured, so on first paint the page
   was laying out against the 72px fallback; anything that made the real bar taller for a
   frame put the page title underneath it until the script caught up. max() takes the
   larger of the constant and the measured value, so the floor holds from the first frame
   and still grows if the bar genuinely is taller. */
body:not(.home) { padding-top: calc(max(72px, var(--header-h, 72px)) + 24px); }
/* body is yellow so the page fade has something to show through. That was invisible
   while the padding exactly matched the bar and sat behind it; now that there is
   clearance, the yellow showed as a strip under the bar. Content pages take white,
   and the darker pages override this later in the file. */
body:not(.home) { background: var(--white); }
/* About is yellow, like the home page. It has to be said again here rather than only by
   coming off the bg-white list above, because that rule is a plain `body` and this one is
   `body:not(.home)`, which out-specifies it and was painting the page white regardless.
   The KitMapper block at the foot keeps its own white ground, exactly as on the home page. */
/* About is white, with equipment and newsletter, not yellow. Set in the white-page list
   above; this override is removed rather than left to fight it. */
header.site a, header.site button, header.site .kit-search { pointer-events: auto; }
header.site .nav-list a, header.site .nav-contact a, header.site .kit-nav-btn { transition: color .3s var(--ease); }
/* home: yellow logo over the hero; nav bar slides in once scrolled past the hero */
.hero-logo { position: absolute; top: clamp(1.1rem, 3vw, 2rem); left: var(--gut); z-index: 4; }
/* DROP-SHADOW REMOVED (Cam, 24 Aug): was `filter: drop-shadow(0 0 6px rgba(0,0,0,.45))`,
   there to hold the wordmark off a light photograph. */
.hero-logo img { height: clamp(30px, 4vw, 46px); width: auto; }
/* HOME HEADER: solid Kitto grey carrying the yellow logo and yellow page names, rather
   than black type floating transparently over whatever the hero image happens to be
   doing behind it. The dividers between the page names come out yellow on their own,
   because they are drawn in currentColor. */
/* The bar you actually see is .header-inner::before, a full-bleed white strip sitting at
   z-index -1. header.site creates a stacking context, so that strip paints ABOVE the
   header's own background and below its text. Setting a background on header.site alone
   looked correct in the computed styles and changed nothing on screen. */
/* The stroke under the bar matches the rules drawn in the bar, so it is yellow now that
   every bar is grey. It was --line, a pale grey, which read as a different, weaker line
   than the dividers sitting directly above it. */
/* THE GREY BAR IS NOW EVERY PAGE'S, 23 Aug 2026. It was the home page's alone, with white
   bars and quieter grey-on-hover type everywhere else, and the two behaviours read as two
   different sites. Cam's call: one bar, grey ground, yellow type, on every page.
   The white-bar rules this replaces have been deleted rather than left to be overridden,
   so there is one description of the header, not two fighting on specificity. */
header.site { background: var(--grey); }
header.site .header-logo img { filter: none; }        /* the SVG is already yellow */
header.site .nav-list a { color: var(--yellow); }
/* THE CURRENT PAGE IS ITALIC, NOTHING IS FILLED (Cam, 23 Aug 2026). Hover and the current
   page used to share one rule that painted a solid yellow button, so every name lit up as
   the pointer crossed it and the bar flickered on the way past. Now the fill is gone
   entirely: the page you are on is marked the same way the breadcrumb trail marks it, in
   italic, and hover italicises too.
   ⚠ The licence covers DIN 2014 Regular and nothing else, so this italic is the browser
   shearing the Regular, not a drawn italic. Same as the breadcrumbs and the home hero. If
   an italic is ever bought, it will pick up here with no change to this rule. */
header.site .nav-list a[aria-current="page"] { font-style: italic; }
/* ITALIC ON HOVER IS ALSO ITALIC ON KEYBOARD FOCUS (10 Sep 2026).
   The italic-on-hover convention was applied inconsistently: the close and toggle
   controls (.hero-stage-close, .checkout-close, .co-mode-toggle, .slider-top,
   .hero-expand) all paired :hover with :focus-visible, while the actual navigation
   did not. So a keyboard user got the cue on a modal's close button and nothing at
   all on the nav, the mobile menu, the category names or the cards, which is the
   wrong way round. Every focusable one now carries both.
   Not added to .kit-card.is-clickable or the .ph / .news-card-media sibling hovers:
   those are an <article> and a <div>, with no tabindex, so they cannot take focus.
   That is a real gap but a different one, and bigger than the italic. */
header.site .nav-list a:hover,
header.site .nav-list a:focus-visible { font-style: italic; text-decoration: none; }
header.site .nav-list a { transition: color .2s var(--ease); }
/* Pin the divider so it stays solid: it is drawn in currentColor, so without this it
   would fade along with the link being hovered and the row would flicker. */
header.site .nav-list li > a::before,
header.site .nav-list li:last-child > a::after { background: var(--yellow); }
/* The rules stay yellow even under a filled button. Flipping them to black drew a hard
   black edge down the left of whatever you were pointing at, which read as damage rather
   than as a fence. Yellow on yellow simply lets the fill run out to its own edges. */
header.site .nav-burger span { background: var(--yellow); }
header.site .nav-basket-plus::before,
header.site .nav-basket-plus::after { background: var(--yellow); }
/* The count is bare type on every other page, so it stays bare type here, just yellow.
   Giving it a yellow fill turned it into a badge nothing else on the site has. */
header.site .nav-basket-count { color: var(--yellow); background: transparent; }
/* THE FILL HAS TO BE IN THIS LIST OR IT DOES NOT ANIMATE (Cam, 24 Aug). transition is one
   property: naming only `transform` here overrode the background-color transition set on
   the header elsewhere, so on the home page the bar's own grey SNAPPED on at the .nav-in
   threshold while the strip behind it faded over .35s and the controls over .3s. Three
   parts of one bar arriving on three different clocks is what read as unsteady. All of them
   are on .55s now, and the snap is gone because the fill is finally in the list. */
body.home header.site { transition: transform .4s var(--ease), background-color .55s var(--ease); }
/* nav bar stays visible on the home page too (no scroll needed to reveal it) */
body.home:not(.nav-in) header.site { transform: none; }
@media (max-width: 768px) { .hero-logo { left: calc(var(--gut) + 1rem); } }

/* header pinned to the bottom on every page (base rule already handles it) */

/* home, over the hero: the top bar is hidden; the logo lives in the hero's bottom-left.
   Scrolling past the hero brings the real nav bar in. */
header.site.nav-over { background: transparent; opacity: 0; visibility: hidden; pointer-events: none; }
header.site.nav-over .nav-list a, header.site.nav-over .nav-contact a { color: var(--yellow); }
header.site.nav-over .header-logo img { height: 52px; transition: height .35s var(--ease); }
/* over the hero: only the logo + enquiries email; full nav returns once scrolled past */
.nav-email { display: none; font-size: var(--fs-title); white-space: nowrap; }
header.site.nav-over .nav-email { display: inline-block; color: var(--yellow); }
header.site.nav-over .nav-right, header.site.nav-over .nav-burger { display: none; }
header.site.nav-over .nav-quote-btn { background: transparent; color: var(--yellow) !important; border: 2px solid var(--yellow); }
header.site.nav-over .nav-quote-btn:hover { background: var(--yellow); color: var(--ink) !important; }
header.site.nav-over .nav-burger span { background: var(--yellow); }

/* home, scrolled past the hero — bar turns black */
header.site.nav-dark { background: var(--ink); }
header.site.nav-dark .nav-list a, header.site.nav-dark .nav-contact a { color: #fff; }
header.site.nav-dark .nav-quote-btn { background: var(--yellow); color: var(--ink) !important; border: 2px solid var(--yellow); }
header.site.nav-dark .nav-quote-btn:hover { background: transparent; color: var(--yellow) !important; }
header.site.nav-dark .nav-burger span { background: #fff; }
/* THE BAR IS FULL WIDTH, SO ITS CONTENTS ARE TOO (Cam, 24 Aug). The header sits in the
   page .container, which caps at 1320px and holds an 80px gutter, while the grey strip
   behind it is drawn full-bleed at 100vw. On a 1600px window that left the + about 220px
   short of the corner of a bar that ran the whole way across, and the wider the screen the
   worse it got, because the cap and the gutter both add to it.
   The cap and the page gutter come off the header only. 24px, so the bar reads as a strip
   with its contents near its corners rather than as page content that happens to have a
   colour behind it.
   ABOVE 992 ONLY. The gutter is already 24px at 992 and 16px on a phone, so there was
   never much space out there to lose, and the basket is not even the last thing in the
   bar below 993, the burger is. Left to run at every width this rule did nothing at 992
   and ADDED 8px on a phone, pushing the burger further from the corner than the page's
   own 16px gutter puts everything else. Mobile keeps var(--gut), as it was.
   The trade, stated plainly: above 993 the wordmark and the nav no longer line up with
   the columns of the page below. That alignment is what the container was buying, and it
   was only ever visible on a wide screen, where the bar's own emptiness was the louder
   problem. */
@media (min-width: 993px) {
  header.site .container { max-width: none; padding-inline: 24px; }
}
.header-inner {
  display: flex; align-items: center; justify-content: space-between; gap: 2rem;
  padding-block: 16px; position: relative;
}
/* Full-width backing behind the nav row only (the search row above stays floating). Grey
   with a yellow stroke since 23 Aug 2026; this is the one place the bar's ground is set,
   and it has to be set HERE rather than in the header colour block further up, because
   this rule is later in the file at equal specificity and silently won. */
.header-inner::before { content: ""; position: absolute; top: 0; bottom: 0; left: 50%; width: 100vw; transform: translateX(-50%); background: var(--grey); border-bottom: 2px solid var(--yellow); z-index: -1; }
/* NO BAR OVER THE HERO (Cam, 24 Aug). The home page now opens with the pictures, and the
   hero band behind them is the same Kitto grey as this strip — so the only thing making the
   header read as a separate bar was its 2px yellow rule, drawing a line across the top of
   the page for no reason. Both go while the reader is still on the hero: the strip's grey
   merges into the band's grey and what is left over the pictures is the yellow wordmark.
   ONLY WHILE OVER THE HERO. main.js adds .nav-in once you scroll past 55% of the band, and
   from there the bar is over white page and needs its ground back to stay legible.
   :not(.basket-open) because this out-specifies the basket's own header rules, and opening
   the basket deliberately paints the bar grey to match the panel coming down. Without the
   exclusion this would win that fight and the panel would arrive attached to nothing. */
/* DESKTOP ONLY (Cam, 24 Aug). This whole behaviour belongs to the expanded, full-bleed
   reading of the hero, and there is no expanding on a phone: the button is hidden below
   768, so a phone never sees the state this was designed around.
   It also cost more there than it gave. The hero on a phone is a swipe strip roughly one
   screen deep, so the bar would vanish and return within a flick, and hiding the controls
   takes the BURGER with them — which on a phone is the only route to the menu, gone for
   the whole of the first screen. On a desktop the nav links are in the bar itself and the
   wordmark still links home, so nothing is unreachable while it is out of the way.
   The bar on a phone stays exactly as it was. */
@media (min-width: 769px) {
  body.home:not(.nav-in):not(.basket-open) .header-inner::before {
    background: transparent;
    border-bottom-color: transparent;
  }
  /* The header's OWN grey, which is a separate fill from the strip above and was still
     painting a band across the top of the pictures after the strip had gone. */
  body.home:not(.nav-in):not(.basket-open) header.site { background: transparent; }
/* AND THE REST OF THE BAR WAITS ITS TURN. With the strip gone the page names, the basket
   and the burger were left standing on the photographs with nothing behind them, which is
   the state the yellow wordmark is designed for and they are not. Over the hero it is the
   logo alone; the moment .nav-in arrives, at 55% of the band, the ground and the controls
   fade in together and the bar is a bar again.
   They fade rather than appear because .nav-in toggles on scroll and can flip back and
   forth around its threshold; a hard switch would blink. The transition is declared on the
   elements themselves, not here, so it runs in BOTH directions rather than only on the way
   out. */
  body.home .nav-right, body.home .nav-burger {
    transition: opacity .55s var(--ease), visibility .55s var(--ease);
  }
  body.home:not(.nav-in):not(.basket-open) .nav-right,
  body.home:not(.nav-in):not(.basket-open) .nav-burger {
    opacity: 0; visibility: hidden; pointer-events: none;
  }
}

/* A desktop burger was tried here on 7 Sep 2026 and taken out again the same afternoon:
   over the hero the bar became the wordmark and a burger, and pressing it pulled the real
   navigation down. It worked, but it made the reader ask for something that should simply
   be there. The answer was not a new control, it was to stop making them wait: the bar now
   returns on the first scroll, see the .nav-in cue in main.js. Nothing of it is left, so
   the burger is a phone control again and .mobile-menu is the phone's drawer alone. */
/* ═══ AND NOW ON A PHONE TOO (Cam, 26 Aug) ═══
   The block above is dated 24 Aug and scoped to desktop for two reasons. One of them has
   since stopped being true.
   It said the phone hero was "a swipe strip roughly one screen deep, so the bar would
   vanish and return within a flick". That was correct about a strip. The phone hero is a
   staggered wall now, several screens deep, and the bar's cue is not a fraction of anything
   anyway: main.js sets .nav-in when the STATEMENT reaches the bar, so the ground stays out
   of the way for exactly as long as there are photographs, then comes back at the first
   thing on the page that is not one. That reads better on the tall wall than it ever did on
   the strip.
   THE BURGER IS THE HALF THAT STAYS. The other objection in that note is still completely
   valid: on a phone the burger is the ONLY route to the menu, and hiding it for the whole
   of the first screen strands the reader. On desktop the page names sit in the bar, so
   nothing is unreachable while the controls are out. So the ground fades and the basket
   fades, and the burger is left alone.
   `.nav-right` on a phone IS just the basket: `.nav-list` inside it is display:none below
   768, so fading .nav-right here takes the plus and nothing else. */
@media (max-width: 768px) {
  body.home:not(.nav-in):not(.basket-open) .header-inner::before {
    background: transparent;
    border-bottom-color: transparent;
  }
  body.home:not(.nav-in):not(.basket-open) header.site { background: transparent; }
  body.home .nav-right {
    transition: opacity .55s var(--ease), visibility .55s var(--ease);
  }
  body.home:not(.nav-in):not(.basket-open) .nav-right {
    opacity: 0; visibility: hidden; pointer-events: none;
  }
}
.header-logo img { height: 38px; width: auto; }
/* 30px between the page names, less between the last name and the basket plus, so the
   plus reads as belonging to the bar rather than as a fourth item in the list. */
.nav-right { display: flex; align-items: center; gap: 16px; }
/* THE + SITS ON THE GUTTER, NOT INSIDE ITS OWN BOX (Cam, 24 Aug). .nav-basket is a 40px
   tap target holding a 21px glyph, so the + is drawn with about 9.5px of its own box to
   the right of it. The page names beside it are plain text and end exactly on the gutter,
   so the + alone was sitting a third of a character short of the line everything else in
   the bar is aligned to, and read as a gap at the end of the row.
   Pulling the control right by that same 9px puts the GLYPH on the gutter while the 40px
   target keeps its full size and simply overhangs into the margin, so nothing is lost to
   the tap.
   ABOVE 992 ONLY, where the basket really is the last thing in the bar and the 9px really
   is the gap before the edge. It was briefly applied at every width on the reasoning that
   the 9px belongs to the button rather than the layout, which is true but beside the
   point: below 993 the burger is last, so all the 9px did there was pull the + towards the
   burger and close a 1.1rem gap the bar is designed around, leaving the two icons reading
   as one crowded cluster. The pair keeps its designed spacing on a phone. */
@media (min-width: 993px) {
  .nav-right > .nav-basket { margin-right: -9px; }
}
.nav-list ul { display: flex; }   /* gap and align-items set below, on the full-height nav */
/* Up one step of the scale. 1rem was off it entirely, sitting between --fs-15 and
   --fs-body while every other size on the site is a token. */
.nav-list a { font-size: var(--fs-body); }

/* THE LINKS ARE THE BUTTONS, and each one is exactly as tall as the bar.
   Three earlier attempts tried to reach the bar's edges by stretching the chain
   header-inner > nav-right > nav-list > ul > li > a. Each one left a gap, because the
   last link in that chain always re-centred: a flex item only stretches if EVERY
   ancestor says so, and one align-items:center anywhere puts the height back to the
   height of the words. So the link is given the bar's height outright instead.
   --header-h is measured from the real header by main.js, so this stays correct if the
   bar ever changes height; the 72px is only the value before that runs. The link
   overflows its 40px content box evenly, top and bottom, which is exactly the bar. */
/* NOT height: var(--header-h). That fed back on itself: the link took the header's
   height, the link plus .header-inner's 16px padding made the header taller, main.js
   re-measured it into --header-h, and the bar grew on every pass.
   Instead the bar's height is stated once as a constant min-height with no padding to
   add to it, and every ancestor stretches, so the link genuinely is the bar. */
.header-inner { padding-block: 0; min-height: 72px; align-items: stretch; }
.nav-right { align-items: stretch; }
.nav-right > .nav-basket { align-self: center; }
.nav-list { display: flex; align-self: stretch; }
.nav-list ul { gap: 0; align-items: stretch; }
.nav-list li { display: flex; align-items: stretch; }
.nav-list a {
  position: relative; display: flex; align-items: center; padding-inline: 18px;
}

/* A rule before every name, so one falls between the logo and Equipment rental, and a
   closing one after the last so About us is fenced off from the basket. Drawn in
   currentColor so it is yellow on the grey home bar, ink on the light ones and white on
   nav-dark, with no per-context rule to keep in step. */
.nav-list li > a::before,
.nav-list li:last-child > a::after {
  content: ""; position: absolute; top: 0; height: 100%;
  width: 2px; background: currentColor;
}
.nav-list li > a::before { left: 0; }
.nav-list li:last-child > a::after { right: 0; }
.header-logo, .nav-burger { align-self: center; }
/* LOGO CENTRED IN THE BAR (Cam, 23 Aug 2026), at every width. Taken out of the flex flow and
   pinned to the middle rather than reordered, so the nav keeps its own run and its dividers
   still meet the edge of the bar.
   TWO THINGS THIS BREAKS IF THEY ARE LEFT OUT. The flow item is gone, so `space-between` has
   only one child and drops the nav to the LEFT; margin-left:auto puts it back on the right.
   And the logo's box is full height and much wider than the wordmark, so without handing
   pointer-events back to the link alone it lies over the nav and swallows clicks meant for it. */
.header-inner > .header-logo {
  position: absolute; left: 50%; top: 50%;
  /* --logo-shift moves the wordmark off dead centre when something else needs the room.
     Transformed rather than re-laid-out, so it slides on the same curve as whatever it is
     making way for instead of jumping when that element finishes animating. */
  transform: translate(calc(-50% + var(--logo-shift, 0px)), -50%);
  transition: transform .35s var(--ease);
  margin: 0; pointer-events: none; z-index: 1;
}
/* THE SHIFT IS GONE (Cam, 24 Aug). The hire pages used to carry the search field IN the
   bar, growing leftwards out of the nav straight into where a centred wordmark sits, so the
   logo stepped 210px left to clear it. The search is its own row under the bar now, the
   nav is three page names again, and there is nothing left for the wordmark to make way
   for: it sits in the middle of the bar on the hire pages exactly as it does everywhere
   else. --logo-shift itself stays in .header-logo's transform, unset and therefore 0, so
   anything that needs to move the wordmark again has the knob to do it with. */
/* LEFT, NOT CENTRED, ON THE NARROWEST SCREENS (Cam, 24 Aug). The wordmark is absolutely
   centred in the bar, and the basket and burger sit on the right taking a fixed 86px
   whatever the screen is. At 38px tall the wordmark is 82px wide, so at 303px it runs from
   110 to 193 and the right-hand group starts at 201: eight pixels apart, and the two read
   as one crowded cluster rather than as the two ends of a bar. Below 400 the logo goes
   back into the flow at the left, where the gap grows with the screen instead of shrinking.
   position: static also hands the link its own box back, so the pointer-events dance the
   absolute version needs is no longer doing anything and can simply not apply. */
@media (max-width: 400px) {
  .header-inner > .header-logo {
    position: static; transform: none; pointer-events: auto; margin-right: auto;
  }
}
.header-inner > .header-logo a, .header-inner > .header-logo img { pointer-events: auto; }
.header-inner > .nav-right { margin-left: auto; }
.nav-quote-btn { font-size: var(--fs-body); }
.nav-contact { display: flex; align-items: center; gap: 22px; flex-shrink: 0; }
.nav-contact a { font-size: var(--fs-15); white-space: nowrap; }
.nav-quote-btn {
  background: var(--ink); color: #fff !important; padding: 8px 16px;
  transition: background-color .3s var(--ease), color .3s var(--ease);
}
.nav-quote-btn:hover { background: #fff; color: var(--ink) !important; }

/* hamburger */
.nav-burger { display: none; width: 30px; height: 21px; position: relative; }
.nav-burger span { position: absolute; left: 0; width: 100%; height: 3px; background: var(--ink); transition: transform .3s var(--ease), opacity .3s var(--ease); }
.nav-burger span:nth-child(1) { top: 0; }
.nav-burger span:nth-child(2) { top: 9px; }
.nav-burger span:nth-child(3) { top: 18px; }
body.nav-open .nav-burger span:nth-child(1) { transform: translateY(9px) rotate(45deg); }
body.nav-open .nav-burger span:nth-child(2) { opacity: 0; }
body.nav-open .nav-burger span:nth-child(3) { transform: translateY(-9px) rotate(-45deg); }

/* equipment sub-bar — no stroke */
.header-subnav { background: var(--yellow); }
.header-subnav .container { padding-block: 0 12px; }
.kit-nav-btn { font-size: var(--fs-15); color: var(--ink); display: inline-flex; align-items: center; gap: 6px; }
.kit-plus { transition: transform .3s var(--ease); }
.kit-nav-btn[data-state="open"] .kit-plus { transform: rotate(45deg); }

/* full-bleed kit dropdown */
.kit-dropdown { background: var(--ink); height: 0; overflow: hidden; transition: height .35s var(--ease); }
.kit-list-wrapper {
  padding-block: 48px; color: var(--on-dark);
  display: grid; align-items: start; gap: 40px;
  grid-template-columns: repeat(5, 1fr) auto;
}
.kit-dropdown h2 { font-size: var(--fs-body); margin-bottom: 1rem; color: #fff; }
.kit-dropdown ul { display: flex; flex-direction: column; gap: 7px; }
.kit-dropdown li a { color: var(--on-dark); font-size: var(--fs-15); }
.kit-dropdown li a:hover { color: var(--yellow); }
.kit-list-col-action { text-align: right; align-self: end; }
.kit-list-col-action a { color: var(--on-dark); font-size: var(--fs-15); }
.kit-list-col-action a:hover { color: var(--yellow); }

/* mobile menu */
.mobile-menu {
  position: fixed; inset: 0 0 0 auto; width: min(86vw, 420px);
  background: var(--grey); color: var(--yellow); z-index: 10000;
  border-left: 2px solid var(--yellow);   /* the panel's own edge, against the page behind */
  transform: translateX(100%); transition: transform .4s var(--ease);
  /* No top padding: the panel sits above the header (z-index 10000 against the header's 50),
     so the burger underneath is covered and the 72px it used to clear was empty yellow. The
     list now starts at the panel edge and runs straight down from Rental. */
  padding: 0 32px 40px; display: flex; flex-direction: column; gap: 1.4rem; overflow-y: auto;
}
body.nav-open .mobile-menu { transform: none; }
/* menu items divided by strokes, list starts at the top */
.mm-nav { display: flex; flex-direction: column; gap: 0; }
/* Rules run the full width of the panel. The panel carries 32px of side padding, so the
   links pull out by that much and pad it back inside, leaving the stroke edge to edge. */
.mm-nav a {
  padding: .95rem 32px; margin-inline: -32px; border-top: 2px solid var(--yellow);
}
.mm-nav a:last-child { border-bottom: 2px solid var(--yellow); }
.mm-search { display: flex; align-items: center; gap: .6rem; border: 2px solid var(--yellow); padding: .5rem .75rem; margin-top: 1.4rem; transition: background-color .18s var(--ease); }
.mm-search input { flex: 1; min-width: 0; background: transparent; border: 0; outline: 0; color: var(--yellow); font-family: var(--font); font-size: var(--fs-body); padding: 0; }
.mm-search input::placeholder { color: rgba(255,237,20,.5); }
.mm-search-go { flex: none; color: var(--yellow); line-height: 0; padding: 0 .2rem; }
/* The old arrow was the &rarr; glyph, which Gill Sans draws soft and small. This is
   drawn geometrically with butt caps and a mitre join, so it stays crisp at any size. */
.mm-search-go svg { display: block; width: 30px; height: 30px; stroke: currentColor;
  stroke-width: 2.5; fill: none; stroke-linecap: butt; stroke-linejoin: miter; }
/* on focus the box fills yellow with black text */
.mm-search:focus-within { background: var(--yellow); }
.mm-search:focus-within input, .mm-search:focus-within .mm-search-go { color: #000; }
.mm-search:focus-within input::placeholder { color: rgba(0,0,0,.45); }
/* Up a step, and onto the scale: 1.2rem was off it, sitting between --fs-body and
   --fs-lead. --fs-lead is only 0.05rem more and would not read as any different, so the
   next real increment is --fs-title. */
.mobile-menu a { color: var(--yellow); font-size: var(--fs-title); }
.mobile-menu a:hover { color: #fff; }
/* Same button behaviour as the grey bar: yellow fill, black type. The fill runs the
   full width because the links already pull out through the panel's side padding.
   Declared AFTER the rule above: same specificity, so order decides. */
.mm-nav a { transition: background-color .18s var(--ease), color .18s var(--ease); }
/* ITALIC ON HOVER (Cam, 26 Aug), the same hover the grey bar's own nav uses at
   `header.site .nav-list a:hover`, so the menu behind the burger answers a pointer the way
   the links it replaces do. The yellow fill stays: it is what carries the hover on a panel
   this size, and the slant is the part that ties the two navs together.
   Not in the transition above on purpose. font-style is not an animatable property, so
   listing it would be inert, and the fill's 180ms is what the eye follows anyway. */
.mm-nav a:hover,
.mm-nav a:focus-visible { background: var(--yellow); color: #000; font-style: italic; }
.mobile-menu .mm-sub { font-size: var(--fs-13); color: rgba(255,237,20,.6); margin-top: 1rem; }
.menu-scrim { position: fixed; inset: 0; background: rgba(0,0,0,.4); opacity: 0; visibility: hidden; transition: opacity .3s; z-index: 9998; }
body.nav-open .menu-scrim { opacity: 1; visibility: visible; }

/* ═══════════════════ HERO — wide image, inset, caption overlaid ═══════════════════ */
/* ═══════════════════ STATEMENT BAND ═══════════════════ */
/* The first thing on the home page. It carries the clearance for the fixed bar, which the
   picture band used to do, so the yellow starts directly under the header rule and the
   line sits in the middle of its own field. */
/* Yellow type on the grey, not black type on a yellow block. The statement and the
   pictures underneath it are then one continuous grey field with a single idea running
   down it, rather than two coloured bands stacked on each other. */
/* THE CLEARANCE HAS MOVED TO THE HERO (Cam, 24 Aug). This band used to open the page, so
   it carried the offset for the fixed bar. The pictures lead now and the statement sits
   under them, so a bar's worth of padding here would be a hole in the middle of the page,
   and the hero has to do the clearing instead. Ordinary section padding, top and bottom. */
.statement-band {
  background: var(--grey);
  padding: clamp(3rem, 6vw, 5rem) 0 clamp(3.5rem, 7vw, 6.5rem);
}

/* ═══ YELLOW OUTLINE AROUND EVERY GREY BAND (Cam, 26 Aug) ═══
   The grey fields were unbounded: where one met the yellow page the edge was just a colour
   change, and where two met each other there was no edge at all, so the hero and the
   statement read as one tall slab of grey rather than as two blocks.
   2px --yellow, the site's stroke weight, and the same stroke the grey header already
   carries along its bottom at `.header-inner::before`. So this is not a new device, it is
   the one already on the bar carried round the rest of the grey.
   THE OUTLINE GOES ROUND A SECTION, NOT ROUND A BAND (Cam, 26 Aug: "no need for a stroke
   between images and an equipment rental service, I consider it part of the same section").
   The pictures and the statement under them are two elements for layout reasons, but they
   are one thing on the page: the work, and the sentence that says what it is. A line
   between them would be dividing a section from itself.
   So the pair is drawn as ONE outlined block. The hero gives up its bottom edge, the
   statement gives up its top, and what is left is a single stroke round the two of them.
   Neither element carries a full border on its own any more, which is why the rule is
   written as an explicit pair rather than as a -2px collapse: there is no shared edge to
   collapse, there is no edge at all. Both selectors only ever match on the home page,
   where the hero is always followed by the statement. */
.hero-band, .statement-band, .newsletter-block { border: 2px solid var(--yellow); }
.hero-band { border-bottom: 0; }
.hero-band + .statement-band { border-top: 0; }
/* THE HERO IS THE TOP OF THE PAGE NOW, so it is what has to clear the fixed bar. Without
   this the first thing on the home page is a photograph with the header sitting on top of
   its top edge. body.home only: on any other page the hero band is not the first element
   and the page's own padding-top already does this. */
body.home .hero-band { padding-top: calc(max(72px, var(--header-h, 72px)) + var(--hero-inset)); }
/* The statement and the picture band are both grey and sit directly on each other, so the
   space between the line and the first picture is the sum of two paddings. Halved on desktop
   (Cam, 23 Aug 2026); the phone keeps its own, where the strip needs the separation. */
/* REPOINTED 26 AUG. This rule is Cam's from 23 Aug and it halved the join between the
   statement and the pictures. It named `.hero-band` padding-TOP and `.statement-band`
   padding-BOTTOM, which were the two touching edges when the statement opened the page.
   The 24 Aug reorder put the pictures first, so the edges that now touch are the hero's
   BOTTOM and the statement's TOP, and neither was being halved: 80px + 75px, 211px of
   empty grey between the last tile and the line, the desktop version of the fault Cam
   reported on the phone.
   The old padding-top line was doing nothing at all, on any page. `body.home .hero-band`
   sets padding-top to clear the fixed bar and is more specific, so it won already, and the
   hero exists nowhere but the home page. Dropped rather than carried.
   The statement keeps its halved BOTTOM as well: that edge is now the join to the journal,
   which is a different gap that nobody has asked to change. */
@media (min-width: 769px) {
  .statement-band { padding-top: clamp(1.5rem, 3vw, 2.75rem); padding-bottom: clamp(1.5rem, 3vw, 2.75rem); }
  .hero-band { padding-bottom: calc(var(--hero-inset) * .5); }
}
/* The statement arrives last, and slower. The yellow overlay clears the home page over 1.15s;
   this waits half a second, then takes 0.9s, so it is still resolving for a quarter second
   after the page is fully up. It is the first thing the site says and it should not have
   finished before the reader has looked at it.
   The delay is what keeps this from being another crossing fade: for the first half second
   the overlay is still opaque over it, so nothing is competing, and only the tail is seen.
   Only the type, not the band: the grey field is the ground the pictures sit on, and fading
   that separately would show a seam between it and the hero below. */
/* backwards, not both. `both` holds the animation's end state on the element for good, and a
   filling animation outranks everything else, so the opacity below could never dim it once
   the page had loaded. `backwards` holds the start state through the delay, which is the bit
   that is actually needed, then lets go. */
body.home .statement-band h1 { animation: pageIn .9s var(--ease) .5s backwards; }
/* Out of the way while a hero picture is expanded. The type fades, and the room it was taking
   closes with it: the band drops to the clearance it has to keep for the fixed bar and
   nothing else, and the line itself stops occupying any height. That is what brings the
   picture up the screen rather than leaving it under a band of empty grey.
   The band keeps its grey throughout, so the field behind the pictures never breaks.
   ONLY THE TYPE EASES. The space closes instantly, and it has to: main.js measures where the
   band has ended up and scrolls to it in the same breath as setting this class, so a padding
   still easing at that moment is measured at its old value and the scroll overshoots by
   however much was left to collapse. It put the top of the picture, and the count and the way
   out with it, a hundred pixels above the top of the screen. Nothing is lost by cutting it:
   this all happens under a hero that is already faded to nothing, and above the fold once the
   scroll lands. */
.statement-band h1 { transition: opacity .26s var(--ease); }
/* The bar's clearance, and then 28px of breathing room before the picture starts. The room has
   to live HERE, not only in the band's scroll-margin: the collapsed statement is the first
   thing in the document, so if it comes to exactly the height of the bar there is nowhere left
   to scroll to and the picture lands hard against it whatever the margin says. */
.statement-band.is-dimmed { padding-block: max(72px, var(--header-h, 72px)) 28px; }
.statement-band.is-dimmed h1 { opacity: 0; height: 0; overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  .statement-band h1 { transition: none; }
}
@media (prefers-reduced-motion: reduce) {
  body.home .statement-band h1 { animation: none; }
}
/* body.home h1 is set to 18px for the section headings on this page, so the statement has
   to out-specify it or it comes out at body size and breaks into four narrow lines. */
body.home .statement-band h1 {
  /* Down 20% from 5.6vw. Every term scales together, minimum, preferred and maximum, so
     the setting is the same setting at a smaller size rather than a different one: the
     line still breaks in the same place and the proportions hold at every width. */
  font-size: clamp(1.28rem, 4.48vw, 3.8rem);
  line-height: 1.04;              /* tight, because at this size the default opens gaps */
  letter-spacing: var(--track-display);   /* tracked out by eye 20 Aug, now the whole band */
  /* The break is in the markup, not left to the measure: two lines, always, split where
     the sentence splits. max-width would re-break it at every window size. */
  max-width: none; text-align: center; text-wrap: balance;
  color: var(--yellow);
  font-weight: 400;
}
/* The journal now follows the statement, and takes the join the pictures used to have: no
   padding of its own, so the only gap is the statement's own bottom padding and the cards
   sit as close under the sentence as the tiles did. var(--sec) on top of that opened a gap
   larger than any other join on the page. */
.statement-band + .newsletter-block { padding-top: 0; }
@media (max-width: 768px) {
  /* THE BAR'S CLEARANCE CAME OUT ON 26 AUG (Cam: too far from the pictures, grey space).
     This used to read `calc(max(72px, var(--header-h,72px)) + 3.5rem)`, written when the
     statement opened the page and had to clear the fixed bar. The 24 Aug reorder put the
     pictures above it, so that 72px was clearing a bar that is no longer there: 128px of
     top padding on top of the hero's own 52px bottom inset, 180px of empty grey between
     the last tile and the line. The desktop rule was fixed at the time, the phone was not.
     Now an ordinary gap. The line belongs to the pictures above it, so the space over it
     is the smaller of the two and the sentence sits with the work it is about. */
  .statement-band { padding: 1.75rem 0 3rem; }
  /* A steeper scale on a phone. 4.48vw is right across a laptop, but on a 390px screen it
     resolves to about 17px and the statement stops being a statement.
     UP AGAIN 24 Aug on Cam's call: 7.6vw resolved to about 29px on a 390px screen, still
     reading as a large paragraph rather than as the page's opening claim. 9.2vw puts it at
     about 36px there. All three terms move together, so it is the same setting larger, not
     a different one. It costs a line of wrap on the narrowest phones, which is the right
     trade for a statement that is meant to be the first thing read. */
  /* UP AGAIN AT THE NARROW END (Cam, 24 Aug). 9.2vw only reaches the 2rem floor below
     about 348px, so on a 288px screen the statement was pinned at 32px and read as the
     smallest it has ever been exactly where it has the least competition for attention.
     The floor carries the increase, and the vw term is raised with it so the size climbs
     out of the floor rather than sitting on it for the whole of the narrow range. */
  body.home .statement-band h1 { font-size: clamp(2.375rem, 11.5vw, 3.25rem); max-width: none; }
  /* And the line break comes out of the markup here. "An equipment rental service" needs
     roughly the full width of the phone at this size, so the forced break either overflowed
     or left one word stranded. Let it find its own breaks, balanced. */
  body.home .statement-band h1 br { display: none; }
}

/* The pictures no longer bleed to the edges: they sit tiled on a white field with an even
   margin all round, so they read as work on a page rather than as a backdrop.
   Knobs: --hero-inset is the margin, --hero-gap the space between tiles, --hero-ratio the
   height of the whole block as a fraction of its width. */
.hero-band {
  --hero-inset: var(--gut);
  /* Half the outer margin. Tight gutters made the five pictures read as one chopped-up
     image; at this width each tile is clearly its own picture, and the gap still reads as
     narrower than the margin round the outside, so the block holds together. */
  --hero-gap: clamp(24px, 2.5vw, 40px);
  --hero-ratio: 0.9;
  --hero-max: 1900px;
  /* The depth of the picture block, held in one place because two things now use it: the
     mosaic, and the expanded carousel that replaces it. Same number for both, so opening a
     picture does not change the height of the band and nothing below the hero moves. */
  --hero-h: calc(var(--hero-ratio) * min(var(--hero-max), 100vw - (2 * var(--hero-inset))));
  /* Kitto grey behind the tiles. White left the pale photographs floating with no edge to
     them; the grey gives every picture a boundary without drawing a line round it. */
  background: var(--grey);
  /* The band used to open the page, directly under the fixed bar, so its top padding had to
     clear that bar. It now sits below the equipment block, in the middle of the page, where
     that clearance would just be a slab of empty grey. Even inset on all four sides. */
  padding: var(--hero-inset);
}
/* The 1900 cap belongs to the pictures, not to the grey. Capping the band itself left the
   yellow page showing down both edges on anything wider than 1900, which is the one colour
   this field exists to keep away from the work. The grey now runs the full width and the
   cap moved inside it, so the arrangement is the size it always was and the field around it
   simply carries on. Minus the insets, because the band's cap included its own padding. */
.hero-mosaic, .hero-stage, .hero-feature {
  max-width: calc(var(--hero-max) - (2 * var(--hero-inset)));
  margin-inline: auto;
}
/* TWO INDEPENDENT COLUMNS, NOT A SHARED ROW GRID. On a grid every tile edge lines up with
   the one beside it, which made the five pictures read as one image cut into pieces. Each
   column stacks on its own instead, the right one starts lower than the left, and the
   proportions differ, so no horizontal edge in one column meets an edge in the other.
   Height is set outright rather than by aspect-ratio: capping the height of an
   aspect-ratio box makes the browser narrow it instead, pulling the tiles off the margin. */
.hero-mosaic {
  --hero-drop: 13%;               /* how far the right column hangs below the left */
  display: grid;
  /* Columns of different widths, so the join between them is not the middle of the page */
  grid-template-columns: 1fr 1.12fr;
  gap: var(--hero-gap);
  /* THE PICTURES SET THE HEIGHT NOW, NOT --hero-h (Cam, 10 Sep: "all the images should try
     to show as much of the aspect ratio as possible, without getting too small or ruining
     the shot"). This row used to be a fixed var(--hero-h) and the tiles divided it up with
     flex-grow, which meant every tile was a shape decided by the layout and object-fit: cover
     threw away whatever did not fit. Two rows of `auto` instead, and each tile carries its
     own photograph's aspect-ratio further down, so cover has nothing left to crop.
     --hero-h is still defined and still used by the phone strip; it is only the desktop
     mosaic that has stopped reading it. */
  grid-template-rows: auto auto;
  height: auto;
  align-items: start;
}
/* PRINKS IS IN THE LEFT COLUMN NOW (Cam, 10 Sep), third, under The Ward Revisited and
   beside the upright Sophie Clements. It was the first child spanning both columns, which
   made it the banner everything else hung under. The grid-column: 1 / -1 that did that
   spanning has gone with it; as an ordinary flex item in .hero-col-left it takes the column
   width, and the 84% below is what makes it read as smaller than the two above it rather
   than as another full-width tile. Offset LEFT so the step is on the same side as the
   column's own edge. */
.hero-feature { width: 100%; margin-right: auto; }
.hero-col { display: flex; flex-direction: column; gap: var(--hero-gap); height: auto; min-height: 0; }
/* THE TILES MUST NOT SHRINK (Safari, 10 Sep 2026).
   With no flex value a flex item is `flex: 0 1 auto`, so flex-shrink is 1 and a column that
   runs tall is allowed to squeeze its items. Squeeze a tile with an aspect-ratio and its
   height stops matching width divided by the ratio, and object-fit: cover suddenly has
   something to take: on Safari "In the Dark, We Play" lost the screen, on Chrome it did not,
   because the two resolve aspect-ratio against flex-shrink differently.
   The ratios above are the photographs' own, chosen so cover has nothing left to crop. That
   contract only holds while the tile keeps its ratio, so say so rather than rely on the
   column never running tall. */
.hero-col > .hero-tile { flex: 0 0 auto; }
/* The drop comes OUT of this column's height. Left as a plain margin it pushed the column
   past the bottom of the band and the last picture ran into the section below: the tiles
   have to keep clear of everything, not just of each other. */
.hero-col-right { margin-top: var(--hero-drop); }
/* EACH TILE IS ITS OWN PHOTOGRAPH'S SHAPE (Cam, 10 Sep). These were flex-grow shares
   dividing a fixed column height, which is what forced every picture through a crop. The
   ratios below are the files themselves, so object-fit: cover has nothing left to take:
     tile-a  work-lloyd-3     1440x1778  ->  4/5
     tile-b  hero-mendel-1    2000x1333  ->  3/2
     tile-c  hero-faramawy-1  2560x1707  ->  3/2
     tile-d  hero-lapelyte-1  2560x1709  ->  3/2
     tile-e  work-clements-1  1499x2000  ->  3/4
     feature work-surel-1     2560x1709  ->  3/2
   IF A PHOTOGRAPH IS REPLACED, ITS RATIO COMES WITH IT. That is the whole contract here,
   and it is why the file sizes are written above rather than left to be rediscovered. */
.tile-a { aspect-ratio: 4 / 5; }
.tile-b { aspect-ratio: 3 / 2; }
.tile-c { aspect-ratio: 3 / 2; }
.tile-d { aspect-ratio: 3 / 2; }
/* Sophie Clements is upright now: work-clements-1 was replaced on 10 Sep with the stacked
   CRTs, which is 3:4 where the old shot was 3:2. */
.tile-e { aspect-ratio: 3 / 4; }
.hero-feature { aspect-ratio: 3 / 2; }
.hero-tile {
  position: relative; overflow: hidden; margin: 0; min-width: 0; min-height: 0;
  background: #1b1b1b;            /* dark placeholder; real image drops in here */
  /* Yellow stroke on the picture edge. outline with a negative offset rather than a
     border, so it is drawn inside the tile and adds nothing to the height the flex shares
     are already dividing up. */
  /* BORDER, NOT OUTLINE (10 Sep 2026). The stroke was an outline at -2px offset so it drew
     inside the tile and added nothing to the height. That worked while the picture was in
     flow. Now the picture is absolute at inset:0 it fills the whole box and paints over an
     inset outline, so the yellow disappeared the moment the Safari fill fix went in.
     A border is inside the box too, box-sizing is border-box globally so the tile is the
     same size either way, and inset:0 resolves against the PADDING box, which is inside
     the border. So the picture fills the tile and the stroke stays on top of nothing. */
  border: 2px solid var(--yellow);
  /* SHADOW REMOVED (Cam, 24 Aug): was `box-shadow: 0 6px 22px rgba(0,0,0,.34)`, described
     here as the lift that stopped the pictures reading as holes cut in the grey. The yellow
     stroke above is now the only thing separating picture from ground. */
}
/* ABSOLUTE, NOT height:100% (Safari, 10 Sep 2026).
   Every tile sets its own aspect-ratio (tile-a 4/5, tile-b/c/d 3/2, tile-e 3/4), so the
   tile's height is derived from its ratio rather than declared. Chrome resolves a
   percentage height against that; SAFARI DOES NOT, so `height: 100%` computed to nothing,
   the picture sat at its intrinsic height and the tile's #1b1b1b showed as a black bar
   under it. object-fit had nothing to crop against.
   Filling the tile by inset instead works in both, because .hero-tile is position:
   relative and the ratio has already given it a definite box. Safe at every width: the
   `aspect-ratio: auto` in the phone block is only a reset before each tile restates its
   own, so no tile ever takes its height from the image. */
.hero-tile img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* The 19 Aug `object-position: 0% center` on tile-e is GONE (Cam, 10 Sep). It existed
   because a 3:2 photograph was being cropped into a 454x347 tile and centring it showed the
   middle of the room. Both halves of that have since stopped being true: the picture is the
   upright CRT stack, and the tile is 3:4 to match it, so there is no crop left to steer and
   an object-position here would only push a correctly-fitted picture off its own centre.
   The phone strip keeps its own 92% further down, where the tile is a different shape. */
/* Each picture is the way in to the kind of kit it was made with: the accessories that
   rigged Very High Frequency, the projectors behind Faramawy and Mendel, the screens at
   Cosmic House, the CRTs in Sophie Clements. The credit block is deliberately left OUT of
   the link, so the photographer's name is a credit and not part of a button. */
/* Absolute for the same reason as the image above: height:100% does not resolve in Safari
   against a height that came from aspect-ratio, so the link collapsed and only part of the
   tile was clickable. inset:0 makes the whole tile the hit area in every browser. */
.hero-tile-link { display: block; position: absolute; inset: 0; }
/* HOVER: the newsletter cards' yellow wash, with a pixel screen over it.
   ::after is the wash, the same flat 50% yellow the news cards use. ::before is the
   screen: a checkerboard on a 6px cell, so the yellow arrives broken into squares rather
   than as a smooth sheet of colour. Both sit below the caption, at z-index 2, so the
   credit stays black on solid yellow instead of sinking into the wash.
   The cell size is the whole effect. Below about 4px it reads as noise, above about 10px
   as a visible grid laid on top; 6px reads as the picture itself going coarse. */
.hero-tile::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: var(--yellow); opacity: 0; transition: opacity .25s var(--ease);
}
.hero-tile::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    repeating-conic-gradient(rgba(0,0,0,.16) 0% 25%, rgba(255,255,255,.10) 0% 50%)
    0 0 / 6px 6px;
  opacity: 0; transition: opacity .25s var(--ease);
}
.hero-tile:hover::after { opacity: .5; }
.hero-tile:hover::before { opacity: 1; }
/* ONE NUMBER SETS HOW FAR THE WALL IS THROWN OUT OF TRUE (Cam, 19 Aug: more offset).
   Every inset below is written as a share of --tile-offset, so the whole composition opens
   up or closes down together and the RELATIONSHIPS between the tiles hold. That matters
   more here than the absolute figures: the arrangements are built so no tile edge in one
   column lines up with an edge in the other, and editing the percentages one at a time is
   how that gets lost. 1 is the set as originally tuned.
   The tiles inherit it from the mosaic, and every calc carries a fallback of 1, so a tile
   outside a .hero-mosaic still lays out. */
.hero-mosaic { --tile-offset: 1.5; }


/* Each tile is also pulled in from one side or the other. Stacked flush, the pictures in a
   column shared a left and a right edge and the block read as a ruled table however the
   heights varied. These insets break every one of those shared edges. The gaps between
   tiles are untouched: this makes the arrangement looser, not tighter.
   These must come AFTER .hero-tile, which resets the figure's default margin and would
   otherwise win on source order and flatten every one of them back to zero. */
.tile-b { margin-right: calc(14% * var(--tile-offset, 1)); }
.tile-c { margin-left: calc(7% * var(--tile-offset, 1)); }
.tile-d { margin-right: calc(11% * var(--tile-offset, 1)); }
.tile-e { margin-left: calc(4% * var(--tile-offset, 1)); margin-right: calc(4% * var(--tile-offset, 1)); }
/* FOUR ARRANGEMENTS, ONE PICKED PER LOAD. main.js sets data-arrangement on the mosaic, so
   the wall is composed differently each visit. The offsets are written out rather than
   generated from random numbers because the offsets ARE the composition: every variant
   keeps the rule the arrangement exists to hold, that no tile edge in one column lines up
   with an edge in the other, and that the two columns pull opposite ways. The rules above
   stay as the default for a load where no attribute is set.

   DESKTOP ONLY, and the media query is load-bearing rather than tidiness. Under 769px the
   mosaic is not a wall at all, it is a one-across swipe strip, and every tile margin is
   reset to a flat 12px gap by `.hero-tile`. These selectors carry a class and an attribute,
   so they outrank that reset wherever they apply: unscoped, they put percentage margins
   back on the tiles of the phone carousel and broke both the alignment and the peek. */
@media (min-width: 769px) {
  .hero-mosaic[data-arrangement="2"] .tile-a { margin-right: calc(9% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="2"] .tile-b { margin-right: 0; margin-left: calc(12% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="2"] .tile-c { margin-left: 0; margin-right: calc(8% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="2"] .tile-d { margin-right: 0; margin-left: calc(13% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="2"] .tile-e { margin-left: calc(6% * var(--tile-offset, 1)); margin-right: 0; }

  .hero-mosaic[data-arrangement="3"] .tile-a { margin-right: calc(13% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="3"] .tile-b { margin-right: calc(4% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="3"] .tile-c { margin-left: calc(11% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="3"] .tile-d { margin-right: 0; margin-left: calc(6% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="3"] .tile-e { margin-left: 0; margin-right: calc(14% * var(--tile-offset, 1)); }

  .hero-mosaic[data-arrangement="4"] .tile-a { margin-right: calc(5% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="4"] .tile-b { margin-right: calc(16% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="4"] .tile-c { margin-left: calc(4% * var(--tile-offset, 1)); margin-right: calc(10% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="4"] .tile-d { margin-right: 0; margin-left: calc(15% * var(--tile-offset, 1)); }
  .hero-mosaic[data-arrangement="4"] .tile-e { margin-left: calc(9% * var(--tile-offset, 1)); margin-right: 0; }
}

/* THE LEAD PICTURE, over the five. Shares .hero-tile, so the yellow stroke, the dark ground
   and the credit block are the same; only the size and the proportion are its own. Pulled
   94% off the right so its left edge lands on neither column and the one edge it shares is
   the right, which tile-d and tile-e already share with each other.
   The gap must be set on `.hero-band .hero-feature`, not `.hero-feature`: `.hero-tile`
   declares `margin: 0` later in the file and at equal specificity that wins, which left the
   lead picture touching the tile below it. */
/* DESKTOP ONLY. Unscoped, these outranked the phone strip's own tile sizing and left the
   lead picture noticeably smaller than every other slide on a phone, where it should just
   be one more slide and nothing special. */
@media (min-width: 769px) {
  .hero-band .hero-feature {
    /* SUPERSEDED 10 Sep. Everything this rule used to do assumed Prinks spanned both
       columns and led the block: 70% to stop it reading as a banner, `margin: 0 auto` to
       centre it because a full-width element has no column edge to align to, and 4/3 to
       put it in the tiles' family of proportions.
       It is now the third picture in the LEFT column, under The Ward Revisited and beside
       Sophie Clements, so all three of those reasons have expired. It has a column edge of
       its own to sit on, and 70% centred inside that column just made it the smallest
       picture in the mosaic (Cam, 10 Sep: "prinks can get bigger").
       Full column width, flush to the column, and 3/2 because that is what work-surel-1
       actually is: 2560x1709. The 4/3 above was cropping it, which is exactly what the
       aspect-ratio pass further up this file exists to stop.
       Kept in this block, and kept at this specificity, ONLY so it still beats the phone
       strip's own sizing at 768 and below. */
    width: 100%; max-width: none;
    margin: 0;
    aspect-ratio: 3 / 2;
  }
}
/* Credit at the top right of its own tile, as a yellow block with black type. Yellow with
   a dark stroke, the treatment used on the other overlaid captions, was legible but it
   floated; a solid block sits on the picture like a label stuck to it, and it never has
   to fight whatever the photograph is doing underneath. */
.hero-tile-cap {
  position: absolute; top: 0; right: 0; z-index: 2; max-width: min(70%, 36ch);
  /* No gap: the leading sets the distance between the lines now, and a gap on top of it
     put the tiles a pixel out from the journal blocks that are meant to match them. */
  display: flex; flex-direction: column; gap: 0; text-align: right;
  background: var(--yellow); color: var(--ink);
  padding: 10px 12px;
  /* TIGHTER THAN THE SHARED --cap-lh (Cam, 10 Sep, after that token had already come down
     from 1.33 to 1.18: "the image captions are still too loose, on the hero images").
     This block is the one place a caption is FOUR stacked lines rather than one wrapped run,
     because gap: 0 means the leading alone sets every gap: the work, the artist and venue
     over two lines, and the photographer. At 1.18 that is 15.3px four times over and the
     block reads as a list of separate lines rather than one label.
     1.06 is 13.8px, close to solid on 13px type, which is right for four short right-aligned
     lines and would be wrong for anything that ran on. The overlaid picture credits keep the
     shared token: they wrap into two or three lines of one continuous sentence, where this
     would be too close. */
  /* 1.06 WENT TOO FAR (Cam, 10 Sep: "I think we made the captions also too tight leading
     wise, we can make that a bit more"). It came down from the shared 1.18 to separate four
     stacked lines from one wrapped run, and close to solid on 13px type turned out to be
     tighter than the block wants. 1.14 keeps it clearly tighter than the wrapped credits and
     gives the four lines room to read as four. */
  font-size: var(--fs-13); line-height: 1.14;
}
/* `balance` was here to stop one line running full width with a single word under it. It
   does even the lines, but it cannot pull the box in with them: this is an absolutely
   positioned flex column, so its width is shrink-to-fit from each span's MAX-content, capped
   by max-width. Balance then sets the lines shorter than that width, and because the type is
   right aligned the whole difference pools as empty yellow down the left.
   `pretty` keeps the orphan control that balance was wanted for while letting each line fill
   the measure, so the block is only as wide as it is actually using. */
.hero-tile-cap span { text-wrap: pretty; }
/* THE CREDIT ARRIVES ON HOVER (Cam, 23 Aug 2026). Six labels sitting on six pictures at once
   was a row of yellow blocks read before any of the work was; one at a time, on the picture
   being pointed at, keeps the strip as pictures and still puts the credit a pointer away.
   Focus counts as hover: the tile's link is keyboard reachable, so tabbing through has to
   surface the same information a mouse does.
   Only where hover is a real input. On a touch screen there is no pointer to rest, so the
   credits stay visible and nothing is hidden behind a gesture that does not exist. */
@media (hover: hover) and (pointer: fine) {
  .hero-band .hero-tile-cap {
    opacity: 0;
    transition: opacity .22s var(--ease);
  }
  .hero-band .hero-tile:hover .hero-tile-cap,
  .hero-band .hero-tile:focus-within .hero-tile-cap { opacity: 1; }
}
/* TABLET. Between the phone strip and a laptop the tiles are still two across but much
   shallower, so a credit sized for a big picture was covering a third of a small one:
   36% of the top-right tile at 883px. The block steps down here, and the two shallow
   tiles on the right give it less room again, so it reads as a label on a picture rather
   than a panel over one. */
@media (max-width: 1100px) and (min-width: 769px) {
  .hero-tile-cap {
    max-width: min(58%, 26ch);
    padding: 7px 9px; gap: 0;
    /* Tighter again here, where the credit is already covering a third of a shallow tile
       and every line of depth it gives back is picture. */
    font-size: var(--fs-12); line-height: var(--lh-tight);
  }
  .tile-c .hero-tile-cap, .tile-d .hero-tile-cap { max-width: min(50%, 24ch); }
}
/* One size for the whole label, the smaller of the two. The work title set larger turned
   each block into a little headline competing with the picture it was sitting on. */
/* One size for the whole label, the smaller of the two, and the work title in italic.
   A title of a work is italicised the way it would be in print, which also separates it
   from the artist and venue without needing a second size or a break. */
.hero-tile-cap .cap-work { font-size: var(--fs-13); font-style: italic; }

/* ─── EXPAND, tile hover → full view ───────────────────────────
   The tiles are crops: every one of these photographs is cut to fit the share of the column
   it was given, and the upright one loses most of its width. This is the way to the whole
   picture. Bottom left, because the credit already has the top right, and the two never
   meet whatever the tile's proportions are.
   Sits above the yellow hover wash (z-index 1) at the caption's level, so it stays black on
   yellow rather than sinking into the wash with the picture. */
.hero-expand {
  position: absolute; left: 0; bottom: 0; z-index: 2;
  display: flex; align-items: center; gap: 7px;
  background: var(--yellow); color: var(--ink);
  padding: 9px 12px;
  font-family: var(--font); font-size: var(--fs-13); line-height: var(--lh-flat);
  cursor: pointer;
  /* Held just off the corner and faded, so it arrives with the wash rather than appearing
     on top of a picture that has not reacted yet. */
  opacity: 0; transform: translateY(6px);
  transition: opacity .25s var(--ease), transform .25s var(--ease);
}
.hero-expand svg {
  display: block; width: 13px; height: 13px;
  stroke: currentColor; stroke-width: 2.5; fill: none; stroke-linecap: butt;
}
/* focus-within as well as hover: the button is reachable by tab, and a control you cannot
   see when it has focus is a control you cannot use from the keyboard. */
.hero-tile:hover .hero-expand,
.hero-tile:focus-within .hero-expand { opacity: 1; transform: none; }
/* Italic on hover (Cam, 10 Sep), the same move as .hero-stage-close, the footer carousel's
   back-to-top and the nav. Two hovers are in play here: the tile's, which brings the button
   in, and the button's own, which is this. Only the <span> shifts, the icon is an SVG and
   font-style does not reach it, so the mark stays put while the word leans.
   As everywhere else on the site, the licence is DIN 2014 Regular only, so this is the
   browser shearing the Regular rather than a drawn italic. It will pick up a real italic
   with no change to this rule if one is ever bought. */
.hero-expand:hover,
.hero-expand:focus-visible { font-style: italic; }
/* No hover on a touch screen, so where the button survives it is simply always there. */
@media (hover: none) {
  .hero-expand { opacity: 1; transform: none; }
}
/* NO EXPANDING AT ALL BELOW 768 (Cam, 10 Sep: "it gets too small"). The stage fits the whole
   picture inside the viewport, so at phone width a 3:2 photograph resolves to a few hundred
   pixels with a credit under it, which is SMALLER than the strip tile it was expanded from
   and the exact failure expanding exists to avoid.
   The tiles below this width are not the hard crop they used to be either: the strip sets
   its own aspect-ratio per tile further down, so there is far less missing than when the
   note above was written and much less reason to offer a way to the uncropped frame.
   Hiding the control is only half of it. main.js also watches this same breakpoint and
   closes the stage if a window that is already showing it is dragged narrower, because CSS
   can hide the way in but cannot put back a view that is already open. */
@media (max-width: 768px) {
  .hero-expand { display: none; }
}

/* ─── EXPANDED CAROUSEL, in place of the mosaic ────────────────
   Same band, same grey, same height: the five tiles are swapped for one large picture and
   the page below does not move. Built from the tiles themselves in main.js, so the credits
   have one source and cannot drift apart.
   contain, not cover: the whole point is the photograph uncut, so it is fitted inside the
   band and the grey carries whatever is left over. The upright one comes out tall and
   narrow, which is the picture's own shape and not a fault. */
/* The band's own depth, but never deeper than the screen. The mosaic is taller than a laptop
   viewport by design, because you scroll down it and meet the five pictures one after
   another. A single picture cannot work that way: at the band's full height the top of the
   photograph and the caption under it were a screen apart, and the whole point here is to
   see the thing at once. So the band contracts while a picture is open and springs back on
   close, and opening scrolls it to the top of the screen, so the reflow happens where the
   reader is already looking rather than under them.
   What comes off the height is exactly what the band puts around the picture on this page:
   the fixed bar, the 12px the scroll stops short of it, and the inset below. Nothing for an
   inset above, because the band has none here, the statement above it carries the clearance.
   Take those off and the whole band lands inside one screen to the pixel, so the picture, its
   credit and the way out are all in view together and nothing has to be scrolled to. */
/* FULL BLEED, AND IT CROPS (Cam, 24 Aug). Everything above describes a CONTAINED view:
   the picture fitted whole inside the band, the grey carrying whatever was left over, and
   main.js measuring each frame in pixels so the stroke sat on the photograph's own edges.
   The result was that expanding a tile often showed a SMALLER picture than the tile did,
   in a box floating in grey, which is why it never read as expanding.
   It fills the window now and crops where the picture and the screen disagree, which is
   what the tiles themselves do and what "expand" is understood to mean. The photographs
   are 3:2 and the window rarely is, so something has to give; giving it to the crop rather
   than to the size is the whole change. Where each picture crops FROM is steerable per
   image with object-position, the same way the mosaic tiles already are.
   Out through the band's inset with a full-viewport width, so it is the screen's width and
   not the container's, and the height is the screen less the fixed bar. */
.hero-stage {
  position: relative;
  /* MAX-WIDTH HAS TO GO, OR NONE OF THE REST OF THIS WORKS (Cam, 10 Sep). .hero-stage is
     in the shared cap at the top of this section alongside .hero-mosaic and .hero-feature,
     which sets max-width to the hero field's width. max-width BEATS width, so the 100vw
     below was being clamped straight back to that cap and the picture came out at the
     field's width instead of the screen's — a grey strip down the right on any window
     wider than the cap.
     The left edge was the same fault seen from the other side: the negative margin below
     resolves to ZERO here, because .hero-band is already full width and 50% and 50vw are
     then the same number. So nothing pulled the short block back to centre and it sat hard
     against the left. Clearing the cap fixes both at once; the margin stays because it is
     still what carries the stage out of the band's inset on any layout where the band is
     NOT full width. */
  max-width: none;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  /* THE BAR GETS ITS HEIGHT BACK (Cam, 10 Sep: "Kitto logo should not be overlapping the
     image on the expand view"). The 24 Aug rule took the whole 100svh and let the fixed bar
     float over the photograph, which was correct while the picture bled off every edge: a
     gap above it would have been a visible band of page and the opposite of full bleed.
     The picture is contained again, so that argument has expired and the overlap is just an
     overlap, with the yellow wordmark sitting on the top of the image.
     Height less the bar, pushed down by the same amount, so the stage begins where the
     header ends and the two never share a pixel. max() against 72px is the same floor the
     rest of the file uses for --header-h before main.js has measured it. */
  --stage-bar: max(72px, var(--header-h, 72px));
  height: calc(100svh - var(--stage-bar));
  margin-top: var(--stage-bar);
}
/* THE WORDMARK STAYS, THE BAR GOES (Cam, 24 Aug). The whole header was faded out first,
   which was too much: with the picture running under the fixed bar the page lost every
   sign of whose site it is at exactly the moment it fills the screen. The logo is yellow
   and reads over a photograph on its own, so it stays and sits ON the picture; what goes is
   everything that needs a bar behind it to be legible — the grey strip, the page names, the
   basket, the burger and the hire pages' search row.
   visibility as well as opacity, so none of it can be clicked or tabbed to while invisible.
   The stage carries its own close button and arrows, and the wordmark is still a live link
   home: header.site a already hands pointer-events back. */
/* ...BUT ONLY WHILE THE PICTURE IS THE SCREEN (Cam, 24 Aug). :not(.nav-in) is what brings
   the bar back once you scroll past the expanded photograph. The stage is 100svh, so the
   moment the statement below it reaches the top main.js sets .nav-in, these rules stop
   matching and the header fades in on its own .55s — the same cue and the same timing as
   the unexpanded page uses, rather than a second mechanism doing the same job.
   The band's padding: 0 further down is deliberately NOT conditioned on this: the picture
   stays full bleed whether or not the bar has returned over it. */
body.hero-expanded:not(.nav-in) .header-inner::before,
body.hero-expanded:not(.nav-in) .nav-right,
body.hero-expanded:not(.nav-in) .nav-burger,
body.hero-expanded:not(.nav-in) .nav-search-row {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .3s var(--ease), visibility .3s var(--ease);
}
/* THE SAME RULE OFF THE STAGE ITSELF, not off a class main.js has to remember to add. The
   stage carries [hidden] when it is closed and drops it when it opens, so "a picture is
   expanded" is already stated in the DOM and the header can read it directly.
   A SEPARATE BLOCK, not another line in the list above: one unsupported selector
   invalidates an ENTIRE selector list, so joining these with a comma would take the
   working .hero-expanded rules down with them on any browser without :has(). Kept apart,
   the two say the same thing and either one is enough. */
body:has(.hero-stage:not([hidden])):not(.nav-in) .header-inner::before,
body:has(.hero-stage:not([hidden])):not(.nav-in) .nav-right,
body:has(.hero-stage:not([hidden])):not(.nav-in) .nav-burger,
body:has(.hero-stage:not([hidden])):not(.nav-in) .nav-search-row {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .3s var(--ease), visibility .3s var(--ease);
}
body:has(.hero-stage:not([hidden])) .hero-band { padding: 0; scroll-margin-top: 0; }
/* THERE ARE TWO GREYS IN THIS BAR, and hiding one leaves the other. .header-inner::before
   is the full-bleed strip, and header.site itself carries `background: var(--grey)` further
   up the file. Everything above only ever hid the strip, so the header's own fill stayed:
   a grey band across the top of a full-bleed photograph, sitting over the expanded view's
   close button and count. Both have to be cleared, and this is the one that was missed. */
body.hero-expanded:not(.nav-in) header.site,
body:has(.hero-stage:not([hidden])):not(.nav-in) header.site { background: transparent; }
/* FULL HEIGHT MEANS THE BAND'S PADDING GOES TOO. The stage breaks out sideways with a 100vw
   width and a negative margin, but vertically it was still sitting inside .hero-band's
   inset — and on the home page inside the bar's clearance on top of that, which is the
   single biggest reason it did not reach the top of the screen. Zero while a picture is
   open; the band gets its padding back on close.
   scroll-margin-top goes with it. main.js reads that property to decide where to scroll the
   band to when a picture opens, and it is set to hold the band clear of the fixed bar. That
   is exactly what full bleed does not want, and the class is applied before the scroll runs,
   so the 0 here is what gets read. */
body.hero-expanded .hero-band { padding: 0; scroll-margin-top: 0; }
/* [hidden] is display:none in the reset, but both of these set a display of their own and
   would win on specificity, so each has to opt back out explicitly. */
.hero-stage[hidden], .hero-mosaic[hidden] { display: none; }
.hero-stage-viewport { position: relative; overflow: hidden; height: 100%; }
/* THE SLIDES ARE STACKED, NOT LINED UP (Cam, 24 Aug). This was a flex row as wide as all
   five pictures, moved sideways by a transform. The expanded view advances on its own now,
   so a picture arriving from the side reads as the page moving rather than as one
   photograph becoming another; they sit on top of each other and cross-fade instead.
   will-change and the transform transition go with it: there is no transform left to
   animate, and leaving will-change on a property nothing changes only asks the compositor
   for a layer it can make no use of. */
.hero-stage-track { position: relative; display: block; height: 100%; }
.hero-stage-slide {
  position: absolute; inset: 0; min-width: 0; height: 100%;
  display: flex; align-items: center; justify-content: center;
  /* 2s against the 5s hold in main.js: three seconds of the picture on its own, then two
     of it becoming the next. Slow enough that the change is something you notice happening
     rather than something you notice has happened. */
  opacity: 0; transition: opacity 2s var(--ease);
  pointer-events: none;
}
.hero-stage-slide.is-current { opacity: 1; pointer-events: auto; }
/* THE VIEW ITSELF FADES IN AND OUT. .is-swapping already takes the band's contents to zero
   while the mosaic and the stage change places, but neither had a transition of its own, so
   it snapped to nothing and snapped back. Both ends are eased now, on the .26s main.js
   times the swap to (FADE), so opening and closing are a fade rather than a cut. */
.hero-stage, .hero-mosaic { transition: opacity .26s var(--ease); }
/* A box the exact size of the picture inside it, so the yellow stroke, the shadow and the
   two labels sit on the photograph's own edges rather than on the grey around it.
   THE SIZE IS SET IN PIXELS BY main.js, not here, and that is deliberate. The obvious CSS,
   an aspect-ratio box with one axis at 100% and the other clamped, is broken in both
   directions: whichever axis is given 100% is definite, so clamping the OTHER one never
   sends the ratio back to re-derive it. On a narrow screen that turned a 3:2 photograph
   into a 0.9 box and cropped it, which made expanding a tile show LESS of the picture than
   the tile did. There is no pure-CSS form of "fit this ratio inside that box" that holds in
   both axes, so main.js measures and writes width and height, on load, on open and on
   resize. The two below are only what stands in for the first frame. */
/* THE FRAME IS THE PICTURE AGAIN (Cam, 10 Sep: "instead of making it absolutely full bleed
   maybe its making it larger and fill its full aspect ratio, the captions are the same
   before even expanding just the full image is shown larger").
   This was width/height 100% for the 24 Aug full-bleed view, with main.js clearing the
   pixel sizes it used to write. Both halves are reverted: main.js measures and writes
   again, and these are only the values that stand in until it has.
   That also settles the stroke, which was the other half of the same note. At full bleed the
   frame ran past the edges of the window, so its 2px outline was partly off-screen and read
   as inconsistent with every other picture on the site; a frame that sits inside the
   viewport shows all four sides, exactly like a .ph or a .hero-tile. */
.hero-stage-frame {
  position: relative;
  width: auto; height: auto;
  max-width: 100%; max-height: 100%;
  /* The tiles' own edge and lift, carried over unchanged: expanding a picture should look
     like the same object made bigger, not like a different treatment. */
  outline: 2px solid var(--yellow); outline-offset: -2px;
  /* SHADOW REMOVED (Cam, 24 Aug): was `box-shadow: 0 6px 22px rgba(0,0,0,.34)`, matched to
     the tile it expands from. The two still match, both now with none. */
}
/* cover, not contain: the frame is already the picture's own proportion, so there is nothing
   to letterbox, and cover guarantees no hairline of background shows inside the yellow
   stroke if the ratio rounds a fraction of a pixel out. */
.hero-stage-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The footer carousel's caption treatment: yellow, black stroke, drop shadow. It carries
   itself over any photograph without a panel behind it, which is why the picture needs no
   scrim under the credit. The colour and the shadow come from the shared overlaid-caption
   rule below, which this is listed in; only the placement is here. */
/* THE BAR IS ON THE BOTTOM (Cam, 24 Aug). It went to the top first, into the corner the
   "1 / 5" count had been holding before it went with the arrows. Down on the bottom edge
   instead, which is where the footer carousel's bar sits and where a caption belongs: the
   top of a photograph is the part you look at first and it should be clear.
   The close button comes down with it and shares the strip, so the two stay one bar rather
   than two pieces of yellow on opposite edges. padding-right clears that button: it is
   about 95px of yellow at the far right of the same strip, and centred text would have run
   under it. Same colour, same 13px, so the credit sits at one end and the way out at the
   other. LEFT aligned for that reason — centring inside a measure that stops 110px short of
   the right would put the type off the middle of the picture anyway. */
/* CENTRED ON THE PICTURE, AND ON ONE LINE WITH THE CLOSE. The padding is symmetrical for
   the centring to be true: it was var(--gut) on the left and 120px on the right, to keep
   the credit clear of the close, and centring inside that box would have put the type well
   left of the picture's actual middle. The same 120px both sides means the box is centred
   on the frame, so its text is too.
   THE VERTICAL PADDING TRACKS THE CLOSE BUTTON'S. Both sit on the same bottom, so they only
   line up while their boxes are the same height: the close is 5px of padding plus a 2px
   border above and below its line, this is the 7px that matches. Change one and the other
   has to move with it. */
/* ONE INSET FOR THE PAIR. The close sits on it and the credit sits a close-button's height
   above it, so the two move together if the inset changes. 2.75rem is that height: 13px of
   type on --cap-lh is about 17, plus 10 of padding and 4 of border is 31, plus a 12px gap.
   Declared on the frame so both children can read it. */
.hero-stage-frame { --stage-inset: clamp(1rem, 2.5vw, 2rem); }
/* THE SAME CAPTION AS ON THE TILE (Cam, 10 Sep: "we should treat captions on the expanded
   view the same as we would without, so the yellow box and black text, potentially on the
   top right like usual").
   This settles a split that had been open since 24 Aug: the site had two ways of captioning
   a picture, a solid yellow block with black type on the tiles and the journal pictures, and
   bare yellow type over the photograph here and on the footer carousel. The block is the one
   that works, because it brings its own ground and is legible over any photograph by
   construction rather than by tuning, and the expanded view is the same picture as the tile
   so it should carry the same label in the same corner.
   Everything below mirrors .hero-tile-cap deliberately: same corner, same colours, same
   padding, same right alignment, same max-width, same 1.06 leading. If that block is
   retuned, this wants the same change. The one thing NOT shared is the hover: on a tile the
   credit arrives on hover, because six labels at once was a row of yellow blocks read before
   the work; here there is one picture and it is always shown.
   The close keeps the bottom right. It is a control, not a credit, and putting it in this
   corner would have two yellow blocks fighting over the same edge. */
.hero-stage-cap {
  position: absolute; top: 0; right: 0; left: auto; bottom: auto; z-index: 2;
  max-width: min(70%, 36ch);
  display: flex; flex-direction: column; gap: 0; text-align: right;
  background: var(--yellow); color: var(--ink);
  padding: 10px 12px;
  font-size: var(--fs-13); line-height: 1.06;
}
/* Stacked, not one running line. These are the SAME three pieces the tile splits over three
   spans, the work, the artist and venue, the photographer, so they break the same way. That
   is why the ", " separator below goes: it existed only to join them back into one sentence
   when they were set inline. */
.hero-stage-cap p { display: block; font-size: var(--fs-13); line-height: 1.06; margin: 0; }
.hero-stage-cap p:not(.cap-sub) { font-style: italic; }   /* the work's title, as in print */
.hero-stage-cap span, .hero-stage-cap p { text-wrap: pretty; }
/* The two labels, on the picture's top corners. Same yellow block as the credit on a tile
   and as the button that opened this, so the way out reads as the counterpart of the way in.
   Which picture you are on goes left, because the tiles' own credits are all on the right
   and the count is not a credit. */
.hero-stage-count, .hero-stage-close {
  position: absolute; top: 0; z-index: 3;
  display: flex; align-items: center; gap: 7px;
  background: var(--yellow); color: var(--ink);
  padding: 9px 12px;
  font-family: var(--font); font-size: var(--fs-13); line-height: var(--lh-flat);
}
.hero-stage-count { left: 0; }
.hero-stage-close { cursor: pointer; }   /* `right` set below, via inset */
/* THE CLOSE BUTTON COMES DOWN WITH THE CAPTION. It keeps right: 0 and sits inside the bar's
   right-hand padding, on the same edge and the same yellow, so the strip reads as one thing
   rather than two pieces of yellow on opposite corners.
   IT HAS TO BE AFTER the shared rule above, not before it: that rule sets `top: 0` at the
   same specificity, so declared earlier this would lose the top and keep the bottom, and the
   button would stretch the whole height of the picture.
   The top edge is left clear on purpose — the Kitto wordmark sits top centre over the
   expanded picture, and it is the only thing up there.
   THE LEADING HAS TO MATCH THE CAPTION'S or the two do not line up. Both boxes take the
   same 9px of padding and the same 13px type, but the credit is set on --cap-lh (about 17px
   of line) and this was on line-height: var(--lh-flat) (13px). That is a 4px difference in height, and
   with both sitting on bottom: 0 it put the button's label 4px below the credit's — near
   enough to read as a mistake rather than as a second element. Same leading, same height,
   one line of type across the bar. */
/* AN OUTLINE, NOT A BLOCK. With the caption's bar gone this was the last solid yellow
   rectangle on the picture. Stroke and type only, so it sits on the photograph the way the
   credit does and still reads as a control because it is the one thing with a box round it.
   It comes in off the corner by the same amount the caption is held off the bottom, so the
   two sit on one line with the picture's own margin around them. */
/* CENTRED, UNDER THE CAPTION. It was in the bottom-right corner, which put the credit and
   the way out at opposite ends of the same line and made the caption reserve 120px it did
   not otherwise need. Stacked instead: credit centred, close centred under it, one axis
   down the middle of the picture.
   left/transform rather than left:0;right:0, so the box stays the width of its own label
   and centres; the corner's `right` has to be released or it would fight the centring.
   SHALLOWER BOX. It was on the 9px of padding the filled block used, which was right when
   it was a solid tab and too deep once it became an outline: a 2px stroke round that much
   air read as a panel rather than a control. 5px is enough to hold the rule off the type. */
/* JUST THE WORD, ON THE BOTTOM RIGHT (Cam, 10 Sep: "close can just be the word close, and
   italics on hover"). It was a centred outlined tab with an X glyph beside the label, which
   said the same thing twice, the cross and the word, and boxed a control that only needs to
   be readable. Everything that made it a tab comes off: the border, the padding, the fill,
   the ground and the icon, which is gone from the markup in main.js rather than hidden here.
   What is left is set exactly like the credit opposite it, same size, same colour, same
   leading, so the two read as one line across the foot of the picture.
   The gut padding matches the caption's so both sit on the same margin as each other. */
/* THE SAME BLOCK AS THE CREDIT, BOTTOM LEFT (Cam, 10 Sep: "close also gets treated the same
   and goes on the bottom left").
   So the expanded picture carries two yellow blocks with black type on opposite corners, the
   credit top right where the tile puts it and the way out bottom left, diagonally clear of
   it. That also fixes the legibility problem the bare yellow type had here: it sat over
   whatever the bottom of the photograph happened to be, and on the Sophie Clements slide
   that is a pale gallery floor, where yellow on white all but disappeared. The block brings
   its own ground, which is the argument for this treatment everywhere else on the site.
   Colours, padding and leading are the caption's, not restated by eye: if .hero-tile-cap or
   .hero-stage-cap is retuned, this wants the same change. */
.hero-stage-close {
  top: auto; bottom: 0;
  left: 0; right: auto; transform: none;
  /* EXACTLY .hero-expand's box (Cam, 10 Sep: "close needs to look the same as expand"), so
     the way out is the same object as the way in: same 9px/12px, same line-height: var(--lh-flat), and
     the 7px gap to the icon comes from the shared rule above. It is one line of type, not
     the caption's stacked three, so it takes 1 rather than the caption's 1.06. */
  line-height: var(--lh-flat);
  padding: 9px 12px;
  background: var(--yellow); color: var(--ink);
  border: 0;
  font-style: normal;
  transition: none;
}
/* Italic on hover, matching the back-to-top on the footer carousel and the italic the work
   titles already take. focus-visible too, so the keyboard sees what the mouse sees. */
.hero-stage-close:hover, .hero-stage-close:focus-visible { font-style: italic; }
.hero-stage-close svg {
  display: block; width: 13px; height: 13px;
  stroke: currentColor; stroke-width: 2.5; fill: none; stroke-linecap: butt;
}
/* Arrows inherit .slider-arrow, and sit on the edges of the band rather than of the picture.
   Which means they fall on the photograph itself when it is wide enough to fill the band, as
   they do on the kit-at-work slides, and out on the grey beside it when it is not. Held to
   the band, they stay in one place from picture to picture; held to the frame they would move
   every time the proportion changed.
   SOLID, not the translucent black they use elsewhere. That 45% black is right over a
   photograph, where it lets the picture through and stays out of the way. Standing on the bare
   grey next to a picture it has nothing to be transparent against, and read as a smudge of
   shade on the field rather than as a control. Solid ink matches the yellow blocks on the
   picture's corners: everything here is a plain rectangle of one colour. */
/* IN THE EXPANDED VIEW THE ARROW IS JUST THE ARROW (Cam, 23 Aug 2026). It was a solid black
   square, which put two hard blocks on either side of the picture you had opened to look at.
   No fill, yellow mark, and hover draws a yellow stroke round the square it already occupies
   rather than filling it or moving it. */
.hero-stage .slider-arrow { background: transparent; color: var(--yellow); border: 2px solid transparent; }
.hero-stage .slider-arrow:hover,
.hero-stage .slider-arrow:focus-visible { background: transparent; color: var(--yellow); border-color: var(--yellow); }
.hero-stage .slider-arrow.prev { left: 0; }
.hero-stage .slider-arrow.next { right: 0; }

/* The swap between the five tiles and the one picture, both ways. The two states are
   different heights and only one is in the document at a time, so this is not a crossfade:
   the band's contents go to nothing, the swap happens in that gap where there is nothing to
   see, and they come back. main.js holds the band's height across the gap and animates it
   between the two, so the page below slides rather than jumps. */
.hero-stage { transition: opacity .26s var(--ease); }
.hero-band.is-swapping .hero-mosaic, .hero-band.is-swapping .hero-stage { opacity: 0; }
/* Only ever set while main.js is holding an explicit height on the band. */
.hero-band { transition: height .32s var(--ease); }
.hero-band.is-resizing { overflow: hidden; }
/* Expanding scrolls the band to the top of the screen, and this is how far short of the top it
   stops. On the home page the band takes no top padding of its own, the statement above it
   holds the clearance for the fixed bar, so without this the top of the picture, the count and
   the way out would all sit under the bar. main.js reads the value off this property.
   The 28 is the breathing room: what shows in that gap is the collapsed statement band, which
   is the same grey, so it reads as the field carrying on above the picture rather than as a
   margin. At 12 the picture was landing hard against the bar. */
.hero-band { scroll-margin-top: calc(max(72px, var(--header-h, 72px)) + 28px); }
@media (prefers-reduced-motion: reduce) {
  .hero-stage-track, .hero-expand, .hero-mosaic, .hero-stage, .hero-band { transition: none; }
}

/* image caption treatment — yellow with a black stroke, used on all overlaid captions */
/* .hero-stage-cap CAME OFF THIS LIST on 10 Sep. It is a solid yellow block with black type
   now, matching .hero-tile-cap, so a `color: var(--yellow)` here would have set yellow type
   on a yellow ground. What is left is the footer carousel, which still sets bare yellow over
   the photograph. */
.slider-cap, .slider-cap .cap-sub, .slider-credit {
  /* ONE CAPTION TREATMENT ON THE SITE (Cam, 24 Aug). This was yellow type with a 0.6px
     black stroke and two drop shadows, a setting invented to survive over an unpredictable
     photograph. It worked, but it meant the site had two ways of captioning a picture: this
     one, and the solid yellow block with black type that .hero-tile-cap and the journal
     article pictures use. Black on yellow everywhere now. It needs no stroke and no shadow
     because it brings its own ground, and it is legible over any photograph by
     construction rather than by tuning. */
  color: var(--yellow);
}
.hero-statement { position: absolute; left: var(--gut); right: var(--gut); bottom: clamp(2rem, 5vw, 4rem); z-index: 3; color: var(--yellow); font-size: 3.75rem; line-height: 1.1; max-width: 22ch; overflow-wrap: break-word; hyphens: auto; }
.hero-statement-logo { height: 2.7rem; width: auto; max-width: none; display: inline-block; vertical-align: -0.4rem; }
@media (max-width: 768px) { .hero-statement { right: var(--gut); max-width: none; } }
/* intro statement (below hero) */
.intro-inner { display: grid; grid-template-columns: 1.1fr 1fr; gap: clamp(1.25rem, 2.5vw, 2.25rem); align-items: start; }
.intro-statement { font-size: var(--fs-statement); line-height: var(--lh-tight); max-width: 18ch; }
.intro-logo { height: clamp(2.4rem, 4.5vw, 3.6rem); width: auto; max-width: none; display: block; }
.intro-block { padding-top: clamp(2.25rem, 4.5vw, 3.75rem); }
.intro-centered { max-width: 58ch; margin-left: clamp(1rem, 6vw, 6rem); text-align: left; font-size: var(--fs-body); line-height: var(--lh-body); }
.intro-aside p { font-size: var(--fs-lead); line-height: var(--lh-body); }
@media (max-width: 768px) { .intro-inner { grid-template-columns: 1fr; gap: 1.25rem; } .intro-statement { max-width: none; } }

.media-credit, .image_credits { margin-top: 8px; text-align: right; color: var(--mid-grey); font-size: var(--fs-13); }

/* ═══════════════════ PRODUCTS (home) ═══════════════════ */
.selections-inner { display: flex; flex-direction: column; gap: clamp(2.5rem, 5vw, 4rem); }
.selections-item { display: grid; grid-template-columns: 1.5fr 1fr 1px 1fr; gap: 2.5rem; align-items: start; }
.selections-item-img-wrap {
  background: #fff; padding: 0;
  display: flex; flex-direction: column; gap: 1rem;
}
.selections-item-img-wrap .ph { aspect-ratio: 5/4; }
.selections-item-desc .header-1 { margin-bottom: 4px; }
.selections-item-characteristics { display: grid; grid-template-columns: 1fr 1fr; gap: .4rem 1rem; margin-block: 1.2rem; }
.selections-item-characteristics .kit-small { line-height: var(--lh-body); }
.selections-item-characteristics .k { color: var(--mid-grey); }
.selections-item-text p { font-size: var(--fs-15); line-height: var(--lh-body); margin-bottom: 1.4rem; }

/* home equipment categories (3-col) */
.home-cat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem 1.6rem; }
/* THE THUMBNAILS ACTUALLY GO AWAY WHEN THE CATALOGUE OPENS (Cam, 19 Aug).
   The toggle sets .hidden on this grid, but the rule above declares display: grid and
   out-specifies the browser's [hidden] { display: none }, so the attribute did nothing and
   the tiles stayed on screen underneath the catalogue rather than being replaced by it.
   Same trap as .ph-viewer and .ph-viewer-cap: any element given a display in CSS has to
   restate its own hidden state. */
.home-cat-grid[hidden] { display: none; }
.home-cat { display: flex; flex-direction: column; }
.home-cat-media {
  background: var(--white); aspect-ratio: 4/3;
  display: flex; align-items: center; justify-content: center;
  color: var(--mid-grey); font-size: var(--fs-13); letter-spacing: .12em; text-transform: uppercase;
  transition: opacity .2s var(--ease);
}
.home-cat-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.home-cat:hover .home-cat-media { opacity: .82; }
.home-cat-body { padding-top: 1rem; display: flex; flex-direction: column; gap: .3rem; }
.home-cat-body .name { font-size: var(--fs-title); }
.home-cat:hover .home-cat-body .name { text-decoration: underline; text-underline-offset: 3px; }
.home-cat-body .model { color: var(--mid-grey); font-size: var(--fs-15); }
/* home Equipment "View all" reuses the equipment page's two-column catalogue (.catalogue / .cat-group) */
#homeKitAccordion { margin-top: 1.5rem; }
/* Drop-down reveal for the catalogue on both the homepage ("View catalogue") and the
   equipment page ("Catalogue view"). Both toggle via [hidden] (display:none→block), so
   re-showing replays the animation.
   THE HEADER ROW IS NOT PART OF THE DROP (Cam, 19 Aug: on the equipment page the button
   looked like it dropped in rather than staying put). On the home page the .section-head
   carrying the title and the button sits OUTSIDE the panel that opens, so it never moves
   and only the list beneath it animates. On the equipment page each panel carries its own
   .section-head, so animating #kitCatalogue itself took the title and the button down with
   it, and the control appeared to arrive rather than stay.
   Targeting the list inside instead makes the two behave alike: the two heads occupy the
   same geometry, so swapping one for the other reads as the label changing in place while
   the catalogue drops in underneath. */
#homeKitAccordion, #kitCatalogue .catalogue { animation: catalogueDrop .45s var(--ease) both; transform-origin: top; }
@keyframes catalogueDrop {
  from { opacity: 0; transform: translateY(-18px); clip-path: inset(0 0 100% 0); }
  to   { opacity: 1; transform: translateY(0);     clip-path: inset(0 0 0 0); }
}
@media (prefers-reduced-motion: reduce) { #homeKitAccordion, #kitCatalogue .catalogue { animation: none; } }
/* Three to two to one, and the first step waits until 900. The gutter has already dropped to
   24px by 992, so at 964 each of three columns is still about 288px — comfortably three
   across, and switching there gave up a row that did not need giving up.
   Dropping a column widens the ones left by half again (267px → 413px at the 900 step), so
   the crop shortens at the same time to offset it; without that the tiles gain about 110px
   of height in one step. The single column takes a height cap on top, because from there on
   extra width should stop turning into extra height. */
/* TWO ACROSS. Bounded at both ends: the orphan rule below must not reach the single-column
   tier, where "half the grid" is half the page. */
@media (max-width: 900px) and (min-width: 577px) {
  .home-cat-grid { grid-template-columns: repeat(2, 1fr); }
  .home-cat-media { aspect-ratio: 16/9; }
  /* An odd count leaves the last tile alone on the bottom row, hard against the left with a
     column of empty space beside it. It spans both columns and centres instead, held to one
     column's width (50% less half the 1.6rem gap) so it stays the same size as the pair
     above.
     FIXED 26 AUG. This was `:last-child:nth-child(odd)` and it silently stopped matching
     the moment the five phone-only categories were added: Screens is still the third card
     and still the last one VISIBLE, but it is no longer the last CHILD, and :last-child
     counts children, not what is displayed. The centred third tile just went back to being
     an orphan hard against the left, which is the effect Cam noticed had gone.
     :nth-child(3) instead, which is what the rule always meant. The desktop page shows
     exactly three cards by design, and the note in index.html is the reason it is three, so
     the third card is a fixed fact about this block rather than a count to be derived.
     :not(.home-cat--swipe) is belt and braces: if the third card is ever a swipe-only one,
     this must not fire on a card that is display:none. */
  .home-cat-grid > .home-cat:nth-child(3):not(.home-cat--swipe) {
    grid-column: 1 / -1;
    justify-self: center;
    width: calc(50% - .8rem);
  }
}
/* ONE ACROSS. The crop goes back to 4/3, the same as every other tier, so nothing is cut
   here that was not already cut at three across. Size is handled by WIDTH instead: the tile
   stops short of full bleed and centres. max-height did the opposite — it pinned the height
   and left object-fit to cover a box the wrong shape, which is what cropped the pictures
   and, on the wider phones, made them taller than they had been. */
@media (max-width: 576px) {
  .home-cat-grid { grid-template-columns: 1fr; }
  .home-cat-media { aspect-ratio: 4/3; max-height: none; }
  .home-cat { max-width: min(340px, 78vw); margin-inline: auto; }
}

/* ═══ THE HIRE BLOCK FADES THROUGH THE CATEGORIES ON A PHONE (Cam, 26 Aug) ═══
   This was a swipe carousel first. Cam's call: "maybe it's not a carousel, it's a fade in
   and out to different kinds of categories." So one card holds the space and the eight
   categories cross-fade through it, rather than sitting in a strip waiting to be pushed.
   It says the same thing without asking for a gesture, and on a block that is one of three
   on the page that is the right amount to ask of a reader.

   ALL EIGHT STACK IN ONE GRID CELL. `grid-area: 1 / 1` puts every card in the same track,
   so the block is as tall as its tallest card and never reflows as the fade moves through
   them. A card of a different height would otherwise make the whole page jump every four
   seconds.

   THE `.is-fading` GUARD IS THE IMPORTANT PART, not a flourish. These rules take every card
   to opacity 0 and only the current one back up, so if the script that adds `.is-current`
   never runs the block would be eight invisible cards and the section would read as broken.
   The class is written by main.js, so no script means no `.is-fading`, none of this applies,
   and the block falls back to the plain stack it has always been on a phone.

   visibility rides along with opacity so a faded-out card is out of the tab order too, and
   it is delayed by the length of the fade on the way out so it does not vanish mid-fade. */
.home-cat--swipe { display: none; }
@media (max-width: 768px) {
  .home-cat--swipe { display: flex; }
  .home-cat-grid.is-fading {
    grid-template-columns: 1fr;
    grid-auto-flow: row;
  }
  .home-cat-grid.is-fading > .home-cat {
    grid-area: 1 / 1;
    max-width: none; margin-inline: 0; width: auto;
    opacity: 0; visibility: hidden;
    transition: opacity .7s var(--ease), visibility 0s linear .7s;
  }
  .home-cat-grid.is-fading > .home-cat.is-current {
    opacity: 1; visibility: visible;
    transition: opacity .7s var(--ease), visibility 0s;
  }
  /* The 2-across orphan rule has no grid to span in a single-cell stack, so it is undone
     here for the 577 to 768 band where both it and the fade are live.
     IT MUST RE-STATE `grid-area: 1 / 1`, NOT `grid-column: auto`. That was the first
     attempt and it broke the whole block: `grid-area` is shorthand, so a later
     `grid-column: auto` reset this card's column back to auto, auto-placement put it in a
     second column, and the grid went from one full-width cell to `101px 341px`. Every card
     was squeezed to a third of the width, the pictures with them, and all eight sat
     overlapping because they were still stacked by row.
     justify-self: stretch for the same reason: the orphan rule centres, and a centred card
     in a single stretched cell is a card narrower than the block. */
  .home-cat-grid.is-fading > .home-cat:nth-child(3):not(.home-cat--swipe) {
    grid-area: 1 / 1; justify-self: stretch; width: auto;
  }
  .home-cat-media { aspect-ratio: 4/3; max-height: none; }
}
/* A fade IS the motion here, so there is nothing to soften: with it off, the block goes
   back to the stack and every category is simply on the page at once. main.js does not add
   `.is-fading` at all under reduced motion, which is what makes this a fallback and not a
   frozen carousel showing one card. */

/* product card grid (category pages) */
.kit-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem 1.6rem; }
.kit-card { display: flex; flex-direction: column; }
.kit-card-media {
  background: var(--paper);
  aspect-ratio: 4/3; display: flex; align-items: center; justify-content: center;
  color: var(--mid-grey); font-size: var(--fs-13);
  overflow: hidden;
}
/* The card only ever held a text placeholder until 14 Aug 2026, so its image child was
   never styled. Without this the img lands in a centring flex container with no size
   and the tile reads as empty even though the file loads fine.
   CONTAIN, not cover: Booqable photos come in whatever shape they were uploaded, and
   a square one (Bendac Dura is 1600x1600) had its top and bottom cropped off in a 4:3
   tile. Contain never crops. Images prepared to 4:3 fill the tile exactly either way. */
.kit-card-media { background: var(--white); }
/* 90%, not 100%: the tile keeps its size and the product sits smaller inside it, so every
   card carries the same air around its photograph whatever shape the Booqable image is.
   One number, and it moves every product card on the site together. */
.kit-card-media img {
  display: block; width: 90%; height: 90%; object-fit: contain;
}
.kit-card-body { display: flex; flex-direction: column; }   /* padding-top and gap set below */
.kit-card-body .name { font-size: var(--fs-body); }
.kit-card-body .model { color: var(--mid-grey); font-size: var(--fs-15); }
/* CATEGORY DESCRIPTIONS ARE BOXED TERMS, NOT A DOTTED LIST (Cam, 19 Aug).
   They were one grey line with middots between: "Projectors · Screens · Surfaces · Mounts".
   Each term is now its own box, which makes them read as the things in the category rather
   than as a sentence about it, and the middots go with the change: the boxes do the
   separating, so a separator inside them would be doing the job twice.
   The stroke is --line and the type stays --mid-gray, the same grey pair the catalogue view
   rules its rows with, and the size is untouched, so these sit at the weight they already
   had rather than becoming a new loud element on the card. */
/* margin-top rather than a gap on the parent: .kit-card-body is set to gap 0 further down
   for the equipment page, and the name and the boxes are the one pair on the card that
   needs air between them. 7px, which is a little over half a line at this size. */
.kit-card-body .model { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.kit-card-body .model .cat-tag {
  border: 2px solid var(--line); color: var(--mid-grey);
  /* 12px, the catalogue view's size (Cam, 19 Aug). The card's own 18px was set for a line
     of prose; as eight separate boxes that weight made the descriptions compete with the
     category name above them. At 12px they read as labels on the card rather than as a
     second heading, which is what .cat-group rows do at the same size. */
  font-size: var(--fs-12);
  padding: 3px 8px; line-height: var(--lh-narrow); white-space: nowrap;
}
.kit-card-body .meta { display: flex; gap: 1.2rem; flex-wrap: wrap; margin-top: .5rem; font-size: var(--fs-15); color: var(--mid-grey); }

/* ═══ NAMES AND TAGS CENTRED UNDER THE PICTURE (Cam, 26 Aug, "in all instances") ═══
   Both card families are here on purpose. `.kit-card-body` is the equipment page's
   category cards AND the Booqable-generated product cards; `.home-cat-body` is the three
   category cards on the home page. They were drawn as one thing and left-aligned as one
   thing, so they centre as one thing rather than drifting apart the next time either is
   touched.
   Three declarations, not one. `text-align` does the type, but `.model` and `.meta` are
   flex rows, and text-align has no effect on flex children: the chips would stay packed
   left under a centred name, which reads worse than leaving the whole card alone. They
   need `justify-content`.
   `.kit-card-foot` is deliberately untouched. It is the price row and it is
   `space-between`, a different job from the name and the tags. */
.kit-card-body, .home-cat-body { text-align: center; }
.kit-card-body .model, .kit-card-body .meta { justify-content: center; }

/* ITALIC NAME ON HOVER (Cam, 26 Aug). The same slant the grey bar's nav and the burger
   menu use, so a pointer gets one answer everywhere on the site.
   BOTH SELECTORS ARE NEEDED. The hire page's category cards are `a.kit-card[href]`,
   written by hand in equipment.html; the product cards are `article.kit-card.is-clickable`,
   generated from Booqable in main.js. One selector would catch half the cards on the site
   and the split would not be visible from either file. */
.kit-card[href]:hover .kit-card-body .name,
.kit-card[href]:focus-visible .kit-card-body .name,
.home-cat:focus-visible .home-cat-body .name,
.kit-card.is-clickable:hover .kit-card-body .name,
.home-cat:hover .home-cat-body .name { font-style: italic; }

.kit-card-foot { margin-top: .8rem; display: flex; align-items: center; justify-content: space-between; }
.kit-card-foot .price { font-size: var(--fs-15); }

/* filter chips */
.chips { display: flex; gap: .6rem; flex-wrap: wrap; margin-bottom: 2.5rem; }
/* SAME SIZE AND COLOUR AS THE CATEGORY TAGS ON THE HIRE PAGE (Cam, 23 Aug 2026). These were
   15px type in a 2px ink box with 8px/16px padding; the tags on the hire page's cards are
   12px in a --line box with 3px/8px. Two sets of small bordered labels a click apart, drawn
   two different ways, read as two systems. They take the tags' measurements exactly.
   The ACTIVE state stays ink-on-white: the tags are static labels and never need one, but a
   filter has to show which filter is on, and that is the one thing these cannot borrow. */
.chip {
  border: 2px solid var(--line); color: var(--mid-grey);
  font-size: var(--fs-12); padding: 3px 8px; line-height: var(--lh-narrow);
  white-space: nowrap; border-radius: 0;
  transition: background-color .25s var(--ease), color .25s var(--ease), border-color .25s var(--ease);
}
.chip:hover { border-color: var(--ink); color: var(--ink); }
.chip.active { background: var(--ink); border-color: var(--ink); color: #fff; }

/* ═══════════════════ EQUIPMENT SEARCH ═══════════════════ */
.kit-search {
  position: relative; display: flex; align-items: center;
  border: 2px solid var(--ink); border-radius: 0;
  padding: 2px 20px; margin-bottom: 2.5rem; background: var(--white);
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
/* Focus darkens the box's own border rather than adding a ring around it. The yellow
   box-shadow that used to do this sat OUTSIDE the border, so clicking the bar in the nav
   put a yellow halo around a grey outline, two strokes of different colours a couple of
   pixels apart. The border still changes, so keyboard focus is not lost. */
.kit-search:focus-within { border-color: var(--ink); }
.kit-search-icon { width: 20px; height: 20px; flex: none; margin-right: 12px; fill: none; stroke: var(--mid-grey); stroke-width: 2; stroke-linecap: round; }
.kit-search input {
  flex: 1; border: 0; outline: 0; background: transparent;
  font-family: var(--font); font-size: var(--fs-body); color: var(--ink);
  padding: 14px 0; min-width: 0;
}
.kit-search input::placeholder { color: var(--mid-grey); }
.kit-search input::-webkit-search-cancel-button { display: none; }

/* ═══ HIRE SEARCH: ITS OWN ROW UNDER THE BAR (Cam, 24 Aug) ═══════════════════════════
   It used to be a fourth <li> inside the nav list, an inline box 180px wide and 30px tall
   wedged between the page names and the basket. Everything above this in the old block was
   the cost of that: a max-width the input could be silently CLIPPED by, an absolute × so
   the clear control cost no width, and three recorded attempts at making a 202px box hold
   202px of content. None of it was the search's fault; it was a form control in a list of
   links, given a slot it could never fit in.
   As its own row it has the whole width of the bar. The clipping problem does not exist,
   the input can be set at body size, and the nav goes back to being three page names.
   THE ROW IS PART OF THE HIRE PAGES, not a panel to be found: main.js only builds it when
   the page is a hire page, so elsewhere there is no element to hide or reason about. It is
   rendered open and drops in on arrival.
   Grey ground, yellow stroke, yellow type, the bar's own scheme. The stroke between it and
   the nav is already there: .header-inner::before carries a 2px yellow bottom border, which
   is the line the bar sits on. This row adds the matching one underneath itself, so the
   search reads as a second register of the bar rather than as the top of the page. */
.nav-search-row {
  background: var(--grey);
  border-bottom: 2px solid var(--yellow);
  pointer-events: auto;
  /* The drop. Height, not transform: the header is measured by a ResizeObserver into
     --header-h, which every page's top padding is calculated from, so the row growing in
     height carries the page down with it instead of landing on top of the first heading.
     A transform would animate nothing the observer can see.
     `both` holds the from-state through the delay, so there is no frame of full-height row
     before it starts. */
  overflow: hidden;
  animation: navSearchDrop .42s var(--ease) .1s both;
  transition: max-height .32s var(--ease), opacity .2s var(--ease);
}
@keyframes navSearchDrop { from { max-height: 0; opacity: 0; } to { max-height: 8rem; opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .nav-search-row { animation: none; max-height: 8rem; opacity: 1; }
}
.nav-search-row > .container { padding-block: 13px; }
/* The field and the catalogue toggle on one line. Both change what the hire page is
   showing rather than navigating away from it, so they sit together and take the same
   treatment; the field keeps its half of the row and the toggle takes the width of its
   own label beside it. */
/* ONE ROW, TOGGLE HARD RIGHT (Cam, 26 Aug: "on the far right, aligned with search
   equipment"). This was briefly stacked with the toggle above the field, which fixed the
   two controls crowding each other but at the cost of a second row of chrome above the
   page. Back on one line, with the crowding solved by distance instead: `margin-left: auto`
   eats every spare pixel between them, so the field sits on the left gutter, the toggle on
   the right, and the row reads as the two ends of the bar rather than as a pair of buttons
   pushed together.
   align-items: center is what "aligned with" means here. Both controls are 46px boxes, so
   they share a centre line and their strokes line up top and bottom.
   The stack is kept for narrow screens further down: at a phone width the two cannot sit on
   one line without the field shrinking to nothing. */
.nav-search-inner { display: flex; align-items: center; gap: 12px; }
.nav-search-toggle { margin-left: auto; }
/* ═══ ONE COMPACT ROW ON A PHONE (Cam, 26 Aug: "takes too much vertical room") ═══
   This was stacked, toggle centred over the field. It read well and cost far too much: two
   40px controls, a 10px gap and 20px of row padding came to 110px of chrome, and with the
   72px nav above it the reader met 182px of furniture before a single product.
   Back to one row, which halves it to about 60px. The crowding that drove the stack in the
   first place is handled by making the TOGGLE small rather than by giving it its own line:
   it drops to 13px in tighter padding, takes only the width of its label, and the field
   takes everything else.
   `min-width: 0` on the field is what makes that safe. A flex item will not shrink below its
   content's intrinsic width without it, so an input with a long placeholder would refuse to
   give ground and push the toggle off the edge of the screen.
   The label is set by main.js and swaps between "Catalogue view" and "Close", so the button
   changes width as it is used; nowrap keeps it on one line while it does. */
@media (max-width: 768px) {
  .nav-search-inner { flex-direction: row; align-items: center; gap: 8px; }
  .nav-search-row > .container { padding-block: 9px; }
  .kit-search.nav-drop {
    flex: 1 1 auto; width: auto; min-width: 0; max-width: none;
  }
  .kit-search.nav-drop:focus-within { width: auto; }
  .nav-search-toggle {
    order: 0; align-self: center; margin-left: 0;
    flex: 0 0 auto;
    height: 40px; padding: 0 12px;
    font-size: var(--fs-13); white-space: nowrap;
  }
}
/* Drawn to the search field's own spec rather than reusing .button: same 46px box, same
   2px yellow stroke, same yellow type on the row's grey, so the pair reads as one control
   split in two. .button would have brought a black fill and white type into a bar that has
   neither. */
.nav-search-toggle {
  flex: none; height: 46px; padding: 0 20px;
  background: transparent; color: var(--yellow);
  border: 2px solid var(--yellow); border-radius: 0;
  font-family: var(--font); font-size: var(--fs-body); line-height: var(--lh-flat);
  cursor: pointer;
  transition: background-color .2s var(--ease), color .2s var(--ease);
}
/* Yellow fill, BLACK type. It filled yellow with the bar's grey, which on a yellow ground
   is the low-contrast pair on the page rather than the high one; --ink is what every other
   yellow block on the site puts its type in. The label reads "Close" while the catalogue is
   open, so this one rule covers the control in both of its states. */
@media (hover: hover) and (pointer: fine) {
  .nav-search-toggle:hover { background: var(--yellow); color: var(--ink); }
}
/* Full width of the row, and tall enough to be typed into rather than aimed at: 46px
   against the old 30, with the input at --fs-body against the old off-scale 14px. */
/* HALF THE ROW, NOT ALL OF IT (Cam, 24 Aug). Run the full width of the bar it was a lot of
   empty box for a field nobody types more than two or three words into, and a 1300px rule
   round a search term reads as a panel rather than a control. Half, held to the left where
   the row starts and where the wordmark and the reading both begin. The height stays: the
   problem was the measure, not the size. */
/* SMALLER AT REST, GROWS WHEN IT IS BEING USED (Cam, 26 Aug: "too wide in some instances,
   we can make it smaller and grow it if it needs to grow").
   This was a flat `width: 50%`, so on a wide screen it came out over 500px of empty field
   sitting in the bar on every hire page whether anyone was searching or not. A search box at
   rest only has to be recognisable; it needs the width when there is a query in it.
   clamp rather than a percentage, so it stops growing on a big monitor instead of tracking
   the window forever, and never falls below something you can type into.
   :focus-within is the cue, so the field opens up the moment it is clicked or tabbed into,
   and settles back when it is left. The width is transitioned, which is why the transition
   below now names it: the box has to be seen to grow, or it reads as the layout jumping. */
.kit-search.nav-drop {
  position: relative; display: flex; align-items: center;
  width: clamp(200px, 26%, 340px);
  height: 46px; margin: 0; padding: 0 16px;
  background: transparent; border: 2px solid var(--yellow); border-radius: 0;
  box-sizing: border-box;
  transition: width .3s var(--ease), border-color .2s var(--ease);
}
.kit-search.nav-drop:focus-within { width: min(50%, 560px); }
.kit-search.nav-drop .kit-search-icon { stroke: var(--yellow); margin-right: 12px; }
.kit-search.nav-drop input {
  flex: 1; min-width: 0; width: auto;
  border: 0; background: transparent; outline: 0;
  font-family: var(--font); font-size: var(--fs-body); line-height: var(--lh-tight);
  color: var(--yellow); padding: 0 26px 0 0;
}
.kit-search.nav-drop input::placeholder { color: var(--yellow); opacity: .65; }
/* Restated for this variant: .kit-search.nav-drop sets the border at the same specificity
   as .kit-search:focus-within, so on source order alone the grey would win and focus would
   do nothing visible. */
.kit-search.nav-drop:focus-within { border-color: var(--yellow); }
/* The mark is DRAWN, for the same reason as .ph-viewer-close: a × character is positioned
   against the baseline, not the centre of its line box, so it measures about 0.11 of the
   font size low and no centring rule can lift it. Two crossed paths are centred by
   construction, and a square box means right: 12px is a real 12px rather than side bearing. */
.kit-search.nav-drop .kit-search-clear {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  flex: none; padding: 0; color: var(--yellow);
  width: 16px; height: 16px;
  display: flex; align-items: center; justify-content: center;
}
.kit-search-clear svg {
  display: block; width: 13px; height: 13px;
  stroke: currentColor; stroke-width: 2.5; fill: none; stroke-linecap: butt;
}
/* ON A PHONE TOO (Cam, 24 Aug). This was desktop-only, on the reasoning that .mm-search in
   the mobile menu already covers it. But that one is behind the burger, so on a phone the
   two things this page is for, finding a piece of kit and switching to the catalogue, were
   both a tap into a menu, while on a desktop they sat in plain view. The row now runs at
   every width and the menu's field stays as the route in from any other page.
   The pair stays on ONE line rather than stacking: two 46px controls under a 72px bar is
   most of the top of a phone screen before any of the page shows. The field gives up its
   half-width and flexes into whatever the toggle does not need, and both come down to 40px,
   which is still a comfortable target. */
@media (max-width: 992px) {
  /* NOT `flex: 1 1 auto` ANY MORE (Cam, 26 Aug). This tier is where "too wide in some
     instances" was actually coming from: `flex: 1 1 auto` made the field eat every pixel the
     toggle did not need, so on a 906px tablet it came out at 726px, far wider than the
     desktop tier above it that Cam had already called too wide. The size went UP as the
     screen got smaller, which is the opposite of what the breakpoint is for.
     Same behaviour as desktop now, sized for this tier: a resting width it does not exceed,
     and the room to grow when it is being typed into. `flex: 0 1 auto` so it can still shrink
     if the row runs out of space, but never grows to fill it. */
  .kit-search.nav-drop { width: clamp(200px, 46%, 420px); flex: 0 1 auto; max-width: none; height: 40px; padding: 0 12px; }
  .kit-search.nav-drop:focus-within { width: min(72%, 520px); }
  .kit-search.nav-drop .kit-search-icon { margin-right: 8px; }
  .kit-search.nav-drop input { font-size: var(--fs-15); padding-right: 22px; }
  .nav-search-inner { gap: 8px; }
  .nav-search-toggle { height: 40px; padding: 0 12px; font-size: var(--fs-15); white-space: nowrap; }
  .nav-search-row > .container { padding-block: 10px; }
}
/* Retract the row instead of dropping it while the basket is down. display:none cannot be
   transitioned, so the row would vanish in a single frame while the basket slid over it,
   which reads as a flicker. animation:none hands the properties back to the transition
   above — an animation with fill-mode `both` would otherwise keep overriding them. */
body.basket-open .nav-search-row {
  animation: none;
  max-height: 0; opacity: 0; pointer-events: none;
}
.kit-search-clear {
  flex: none; font-size: 1.6rem; line-height: var(--lh-flat); color: var(--mid-grey);
  padding: 0 0 0 12px; transition: color .2s var(--ease), opacity .2s var(--ease);
}

.kit-search-clear:hover { color: var(--ink); }
.kit-empty { font-size: var(--fs-lead); color: var(--mid-grey); padding: 1rem 0 2rem; }

/* catalogue index — two columns, 12px */
.catalogue { column-count: 2; column-gap: clamp(2rem, 5vw, 5rem); }
@media (max-width: 640px) { .catalogue { column-count: 1; } }
.cat-group { break-inside: avoid; margin-bottom: 1.8rem; }
/* THE CATALOGUE WAS SET BELOW THE SCALE (Cam, 24 Aug). Both the group heading and the rows
   were on a hardcoded 12px, which is not a step in the type system at all: the smallest
   token is --fs-13. So the one view on the site that is nothing but a long list of names
   and prices was also the smallest type on it. --fs-15 is the next real step up and a 25%
   increase, and both move together so the heading and its rows keep the relationship they
   had, with the rule and the ink colour still doing the separating rather than a size
   difference. --fs-body is the step above if this is still short. */
.cat-group h3 { font-size: var(--fs-15); color: var(--ink); margin-bottom: .4rem; padding-bottom: .35rem; border-bottom: 2px solid var(--ink); }
.cat-group ul { display: flex; flex-direction: column; }
.cat-group li { border-bottom: 2px solid var(--line); }
.cat-group li button, .cat-group li a { display: flex; width: 100%; justify-content: space-between; gap: 1.5rem; align-items: baseline; text-align: left; background: none; border: 0; padding: 5px 0; font-family: var(--font); font-size: var(--fs-15); color: var(--ink); cursor: pointer; }
/* Italic on hover, not underline (Cam, 23 Aug 2026), matching the nav bar. These rows
   already carry a rule under every one of them, so an underline on hover put a second line
   a couple of pixels above the first and read as a doubled border rather than as a link. */
.cat-group li button:hover .cat-name, .cat-group li a:hover .cat-name,
.cat-group li button:focus-visible .cat-name, .cat-group li a:focus-visible .cat-name { font-style: italic; text-decoration: none; }
.cat-price { color: var(--ink); white-space: nowrap; }

/* home + equipment + newsletter + about: consistent 18px type, headers included (12px stays
   for index/meta). About was missing from this list, so that page alone ran at the browser's
   default 16px. Nothing on it declares its own size, so everything inherited that, and the
   KitMapper block, which is the same markup as the home page's, came out a size smaller,
   the "Learn about KitMapper" link most visibly. 18px is the base the whole site is set in. */
body.home main, body[data-page="equipment"] main, body[data-page="newsletter"] main,
body[data-page="about"] main { font-size: var(--fs-body); }
body.home h1,
body.home .sub { font-size: var(--fs-body); line-height: var(--lh-narrow); }
/* section subheaders — bring up to the title increment (matches the newsletter article h2), not the old 18px */
body.home .section-head .header-1, body[data-page="equipment"] .section-head .header-1, body[data-page="newsletter"] .section-head .header-1 { font-size: var(--fs-title); line-height: var(--lh-tight); }
body.home .home-cat-body .name, body.home .home-cat-body .model,
body[data-page="equipment"] .kit-card-body .name, body[data-page="equipment"] .kit-card-body .model { font-size: var(--fs-body); }
.kit-search.in-nav input { font-size: var(--fs-body); }

/* product card (search results + kit pages): clickable open area + a '+' add button */
.kit-card.is-clickable { position: relative; outline: 2px solid transparent; outline-offset: 7px; transition: outline-color .18s var(--ease); }
.kit-card.is-clickable:hover { outline-color: var(--ink); }
.kit-card-open {
  display: block; width: 100%; text-align: left; background: none; border: 0; padding: 0;
  font: inherit; color: inherit; cursor: pointer;
}
.kit-card-open:focus-visible { outline: 2px solid var(--ink); outline-offset: 7px; }
.kit-card-add {
  position: absolute; top: 10px; right: 10px; z-index: 2; width: 34px; height: 34px; border-radius: 0;
  background: var(--ink); border: 2px solid var(--ink); cursor: pointer; font-size: 0;
  transition: background-color .18s var(--ease);
}
.kit-card-add::before, .kit-card-add::after { content: ""; position: absolute; top: 50%; left: 50%; background: var(--white); transition: background-color .18s var(--ease); }
.kit-card-add::before { width: 14px; height: 2px; margin: -1px 0 0 -7px; }
.kit-card-add::after  { width: 2px; height: 14px; margin: -7px 0 0 -1px; }
.kit-card-add:hover, .kit-card-add:focus-visible { background: var(--white); }
.kit-card-add:hover::before, .kit-card-add:hover::after,
.kit-card-add:focus-visible::before, .kit-card-add:focus-visible::after { background: var(--ink); }

/* Search results fade in and out instead of snapping. Applied by main.js, which holds the
   matching 180ms; change one and change the other. */
.fade-swap { opacity: 0; transition: opacity .18s var(--ease); }
/* Search result pictures fade in as they load. Armed by .fades-images, which main.js puts
   on the one grid it rebuilds, so a grid it does not manage can never be left blank. */
.kit-grid.fades-images img { opacity: 0; transition: opacity .45s var(--ease); }
.kit-grid.fades-images img.is-loaded { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .kit-grid.fades-images img { opacity: 1; transition: none; }
}
.fade-swap.fade-in { opacity: 1; }

/* product card type lockup: name (ink) / type (grey) / price (grey) */
.kit-card-body { padding-top: 1.1rem; gap: 0; }
.kit-card-open .name  { font-size: var(--fs-body); line-height: var(--lh-narrow); color: var(--ink); }
.kit-card-open .type  { font-size: var(--fs-body); line-height: var(--lh-narrow); color: var(--mid-grey); }
.kit-card-open .price { font-size: var(--fs-12); line-height: var(--lh-tight); color: var(--mid-grey); margin-top: .8rem; }

/* ═══════════════════ EQUIPMENT DETAIL MODAL ═══════════════════ */
body.kit-modal-open { overflow: hidden; }
.kit-modal { position: fixed; inset: 0; z-index: 200; display: flex; align-items: center; justify-content: center; }
.kit-modal[hidden] { display: none; }
.kit-modal-scrim { position: absolute; inset: 0; background: rgba(20,20,20,.55); backdrop-filter: blur(2px); animation: kitFade .25s var(--ease); }
.kit-modal-panel {
  position: relative; z-index: 1; background: var(--white); width: min(560px, 92vw);
  /* The PANEL no longer scrolls. Scrolling the whole thing dragged the product photo
     out of view as soon as the specs ran long, which is the one thing the reader wants
     to keep looking at. The image column is now fixed and only the text scrolls. */
  max-height: 90vh; overflow: hidden; display: grid; grid-template-columns: 1fr 1fr;
  animation: kitRise .3s var(--ease);
  /* Yellow stroke, the same treatment the hero tiles and every journal picture carry. Inset
     with a negative offset rather than a border so it sits inside the panel's own box and
     does not add to the width the grid columns are measured from. */
  outline: 2px solid var(--yellow); outline-offset: -2px;
}
/* Only the text column scrolls, and it owns the panel's height. min-height: 0 is what
   lets a grid child actually shrink and scroll rather than stretching the row. */
.kit-modal-body { overflow-y: auto; min-height: 0; max-height: 90vh; }
@keyframes kitFade { from { opacity: 0; } }
@keyframes kitRise { from { opacity: 0; transform: translateY(12px); } }
.kit-modal-close {
  position: absolute; top: 14px; right: 16px; z-index: 3; font-size: 0; line-height: 0;
  width: 40px; height: 40px; border-radius: 0; background: rgba(255,255,255,.85); color: var(--ink);
  display: flex; align-items: center; justify-content: center; transition: background-color .2s var(--ease);
}
/* × drawn from two centred bars so it sits dead-centre on the yellow */
.kit-modal-close::before, .kit-modal-close::after { content: ""; position: absolute; top: 50%; left: 50%; width: 17px; height: 2.5px; background: currentColor; }
.kit-modal-close::before { transform: translate(-50%, -50%) rotate(45deg); }
.kit-modal-close::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.kit-modal-close:hover { background: var(--yellow); }
.kit-modal-media {
  /* White, not paper. Product shots are cut out on white, so anything off-white leaves
     a visible rectangle where the photo ends, which is worse now the image fits to
     width and no longer fills the panel. */
  background: var(--white); min-height: 340px; display: flex; align-items: center; justify-content: center;
  color: var(--mid-grey); font-size: var(--fs-13); letter-spacing: .12em; text-transform: uppercase;
  /* Fill the panel's full height so the image can sit dead centre in it. Without this
     the media column only ever grew to the image's own height and the picture sat at
     the top while the text column ran on below it. */
  height: 100%; align-self: stretch;
}
/* Fill the WIDTH and let the height follow, rather than cover, which forced the image to
   the panel height and cropped it. Product shots are objects on a plain ground, so the
   whole object has to be visible; cropping a projector to fill a box is worse than a
   little space above and below it. */
/* Fits to width, keeps its proportions, and is centred vertically by the flex parent
   whatever its shape. margin auto does the centring even if the flex context changes. */
.kit-modal-media img { width: 100%; height: auto; max-height: 100%; object-fit: contain; display: block; margin: auto; }
.kit-modal-body { padding: clamp(1.75rem, 3vw, 3rem); }
/* CATEGORY, NAME AND PRICE ARE ONE BLOCK, SET AS THREE CONSECUTIVE LINES.
   They were three: all 18px, but on three different leadings, .kit-modal-cat inherited 1.2
   from .header-1, .kit-modal-title declared 1.25 and .kit-modal-price fell through to the
   global p at 1.5. Then 8px of margin was added between each. So the baseline-to-baseline
   step went 30px, then 25px, for three lines of the same size, and the stack read as three
   separate things pushed apart rather than one label.
   One leading and no margins between them, so the steps are equal and the block sets like
   copy. The space that used to be inside the stack is kept below it, where the description
   starts, which is the one gap that is doing real work. */
.kit-modal-cat, .kit-modal-title, .kit-modal-price {
  font-size: var(--fs-body); line-height: var(--lh-narrow); margin: 0; text-align: center;
}
/* The whole stack is centred (Cam, 19 Aug). Only the category is italic: it is the one
   line that names a section of the catalogue rather than the object in front of you, and
   italic is already how this site marks a title belonging to something else, see
   .hero-stage-cap. The name and the price are the object itself, so they stay roman.
   A blank line under it, exactly one line of the leading the stack is set on, so the break
   sits on the same rhythm as the text rather than being an arbitrary gap. */
.kit-modal-cat {
  color: var(--mid-grey); font-style: italic;
  margin-bottom: calc(var(--lh-narrow) * 1em);
}
.kit-modal-price { margin-bottom: 1.25rem; }
/* expanded card / detail view: all 18px */
.kit-modal-cat, .kit-modal-overview li, .kit-modal-specs dt, .kit-modal-specs dd { font-size: var(--fs-body); }
.kit-modal-desc { font-size: var(--fs-body); line-height: var(--lh-body); color: #333; margin-bottom: 1.5rem; }
.kit-modal-desc p { font-size: var(--fs-body); line-height: var(--lh-body); }
.kit-modal-overview { list-style: none; display: flex; flex-direction: column; gap: .5rem; }
.kit-modal-overview li { position: relative; padding-left: 1.1rem; font-size: var(--fs-12); line-height: 1.5; color: var(--ink); }
.kit-modal-overview li::before { content: "—"; position: absolute; left: 0; color: var(--mid-grey); }
/* The rules run the full width of the card, not the width of the text. .kit-modal-body
   carries a clamp of padding, so the spec list pulls back out through it with a matching
   negative margin and pads its own rows back in, which leaves the strokes edge to edge while
   the type stays on the same measure as everything above it. Thicker at 3px: at 2 they read
   as the same weight as the hairlines inside the equipment cards, and here they are dividing
   the card rather than detailing it. */
.kit-modal-specs {
  display: grid; gap: 0; margin-bottom: 1.5rem;
  margin-inline: calc(-1 * clamp(1.75rem, 3vw, 3rem));
  border-top: 3px solid var(--line);
}
.kit-modal-specs > div {
  display: grid; grid-template-columns: 8rem 1fr; gap: 1rem;
  padding: .6rem clamp(1.75rem, 3vw, 3rem);
  border-bottom: 3px solid var(--line);
}
.kit-modal-specs dt { color: var(--mid-grey); font-size: var(--fs-15); }
.kit-modal-specs dd { font-size: var(--fs-15); }
.kit-modal-insitu-head { font-size: var(--fs-15); color: var(--mid-grey); margin-bottom: .6rem; }
.kit-modal-insitu-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: 1.5rem; }
.kit-modal-insitu-grid img { width: 100%; aspect-ratio: 3/2; object-fit: cover; display: block; }
.kit-modal-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem 1rem; }
@media (max-width: 768px) {
  /* Stacked, so the split-scroll makes no sense: the image is above the text, not beside
     it, and pinning it would eat the screen. The whole panel scrolls again here. */
  .kit-modal-panel { grid-template-columns: 1fr; width: 94vw; overflow: auto; }
  .kit-modal-media { min-height: 200px; aspect-ratio: 4/3; height: auto; }
  .kit-modal-body { overflow-y: visible; max-height: none; }
}

/* The in-situ full-bleed viewer and its styles came out with the "See this in situ" button,
   which was the only thing that opened it. The in-use photographs stay on the card, in the
   grid above the actions (.kit-modal-insitu-*, above). */

/* ═══════════════════ BASKET ═══════════════════ */
.nav-basket { position: relative; display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; }
/* '+' drawn from two centred bars so it rotates true into an × */
.nav-basket-plus { position: relative; display: block; width: 22px; height: 22px; font-size: 0; transform-origin: center; transition: transform .3s var(--ease); }
.nav-basket-plus::before, .nav-basket-plus::after { content: ""; position: absolute; top: 50%; left: 50%; background: var(--ink); }
.nav-basket-plus::before { width: 21px; height: 3px; margin: -1.5px 0 0 -10.5px; }
.nav-basket-plus::after  { width: 3px; height: 21px; margin: -10.5px 0 0 -1.5px; }
.nav-basket[aria-expanded="true"] .nav-basket-plus { transform: rotate(45deg); }   /* + becomes × when the basket is open */
/* UP FROM 11px (Cam, 24 Aug). This is the only thing in the bar that reports state, and
   at 11px it was both the smallest type on the site and set below the scale entirely. It
   is a superscript on a 21px glyph, so it cannot take a body size without becoming a
   second mark competing with the +; --fs-13 is the smallest real step and enough to be
   read at a glance. The offsets come in with the size, or the wider numeral would push
   further out past the corner of the + than it did before. */
.nav-basket-count {
  position: absolute; top: -4px; right: -4px; background: transparent; color: var(--ink);
  font-size: var(--fs-13); line-height: var(--lh-flat);
}
.basket-scrim { position: fixed; inset: 0; z-index: 44; background: rgba(20,20,20,.4); opacity: 0; transition: opacity .35s var(--ease); }
.basket-scrim.show { opacity: 1; }

/* while the basket is open, the header adopts the basket's grey/yellow scheme and fades back to white on close */
.header-inner::before { transition: background-color .55s var(--ease), border-color .55s var(--ease); }
header.site .header-logo img { transition: filter .35s var(--ease); }
.nav-burger span { transition: transform .3s var(--ease), opacity .3s var(--ease), background-color .35s var(--ease); }
.nav-basket-plus::before, .nav-basket-plus::after { transition: background-color .35s var(--ease); }
.nav-basket-count { transition: color .35s var(--ease); }
body.basket-open .header-inner::before { background: var(--grey); border-bottom-color: var(--yellow); }
body.basket-open header.site .header-logo img { filter: none; }                              /* yellow wordmark */
body.basket-open header.site .nav-list a { color: var(--yellow); }
body.basket-open header.site .nav-burger span { background: var(--yellow); }
body.basket-open header.site .nav-basket-plus::before,
body.basket-open header.site .nav-basket-plus::after { background: var(--yellow); }
body.basket-open .nav-basket-count { color: var(--yellow); }
/* Pointing at the basket lifts the + / × so the control answers the cursor without the bar
   around it changing. The lift has to follow the colour the + already is: on an ordinary
   page it is ink on a light header, so it greys like every other control. The pale yellow
   below is kept only where the + is ALREADY yellow — the home hero and while the basket is
   open — because on a white header it was pale-yellow-on-white and read as the icon
   dropping out rather than responding. */
/* ONLY WHERE THERE IS A POINTER TO HOVER WITH (Cam, 24 Aug). A touch screen has no hover
   state to leave: tapping the basket applies :hover and it STAYS applied until something
   else is tapped, so the + went grey on tap and sat there grey, which read as the control
   having been disabled rather than used. Behind a hover query the rule cannot fire on a
   phone at all, and the + keeps whatever colour the bar it is in gives it. */
@media (hover: hover) and (pointer: fine) {
  header.site .nav-basket:hover .nav-basket-plus::before,
  header.site .nav-basket:hover .nav-basket-plus::after { background: var(--mid-grey); opacity: 1; }
  header.site .nav-basket:hover .nav-basket-count { color: var(--mid-grey); }

  body.home header.site .nav-basket:hover .nav-basket-plus::before,
  body.home header.site .nav-basket:hover .nav-basket-plus::after,
  body.basket-open header.site .nav-basket:hover .nav-basket-plus::before,
  body.basket-open header.site .nav-basket:hover .nav-basket-plus::after { background: #fff79e; opacity: 1; }
  body.home header.site .nav-basket:hover .nav-basket-count,
  body.basket-open header.site .nav-basket:hover .nav-basket-count { color: #fff79e; }
}
.basket-scrim[hidden] { display: none; }
/* basket slides UP from the bottom nav */
.basket-panel { position: fixed; left: 0; right: 0; top: 0; z-index: 46; background: var(--white); color: var(--ink); max-height: 82vh; overflow-y: auto; border-bottom: 2px solid var(--ink); transform: translateY(-100%); transition: transform .42s var(--ease); }
.basket-panel.open { transform: translateY(0); }
.basket-panel[hidden] { display: none; }
.basket-panel .container { padding-block: 2rem 2.5rem; }
.basket-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.25rem; }
.basket-head .header-1 { font-size: var(--fs-body); }
.basket-foot .button, .basket-foot .u { font-size: var(--fs-body); }
.basket-close { font-size: 1.9rem; line-height: var(--lh-flat); color: #fff; transition: color .2s var(--ease); }
.basket-close:hover { color: var(--yellow); }
.basket-list { display: flex; flex-direction: column; }
/* Third track is `auto`, not a fixed 1.5rem: the remove control is now a padded flex box
   around a drawn glyph, and a fixed track narrower than its content pushed it past the row's
   own right edge. */
.basket-item { display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: clamp(1.5rem, 5vw, 4rem); padding: .9rem 0; border-top: 2px solid var(--line); }
.basket-item:last-child { border-bottom: 2px solid var(--line); }
/* Rows fade as they arrive and leave. The row also collapses its own height and padding,
   so the ones below slide up to meet it instead of jumping. grid-template-rows is what
   animates: a plain height cannot be transitioned from auto. */
@keyframes basketIn {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes basketOut {
  from { opacity: 1; transform: none; max-height: 8rem; padding-block: .9rem; }
  to   { opacity: 0; transform: translateX(10px); max-height: 0; padding-block: 0; border-top-width: 0; }
}
.basket-item.is-new { animation: basketIn .26s var(--ease) both; }
.basket-item.is-leaving { animation: basketOut .24s var(--ease) both; overflow: hidden; pointer-events: none; }
@media (prefers-reduced-motion: reduce) {
  .basket-item.is-new, .basket-item.is-leaving { animation: none; }
}
.basket-item-info { min-width: 0; }
.basket-item-name { display: block; font-size: var(--fs-body); }
/* CATEGORY AND PRICE COME OUT OF THE BASKET (Cam, 24 Aug). The basket is the running
   tally: what is in it, how many, and a way to take each one back out. The type and the
   per-day price under every name are what you compare things by while choosing, and they
   are still on the equipment pages where that happens and at the checkout where the order
   is read back. Between those two the basket only has to answer "what have I got".
   Hidden rather than cut from main.js, so the markup is unchanged and this is one line to
   put back. */
.basket-item-meta { display: block; color: var(--mid-grey); font-size: var(--fs-body); margin-top: 2px; }
/* qty stepper */
.basket-qty { display: inline-flex; align-items: center; gap: 1rem; }
.basket-qty-btn { position: relative; width: 26px; height: 26px; flex: none; border: 2px solid var(--ink); border-radius: 0; font-size: 0; line-height: var(--lh-flat); padding: 0; color: var(--ink); transition: background-color .15s var(--ease), color .15s var(--ease); }
/* smart basket — triage recommendations */
.basket-recs { padding-top: 1.1rem; }   /* border-top and margin-top set below */
.basket-recs-title { font-size: var(--fs-body); color: var(--mid-grey); margin-bottom: .9rem; }
.basket-rec-btns { display: flex; flex-wrap: wrap; gap: .55rem; }
.basket-rec-btn { font-size: var(--fs-12); line-height: var(--lh-flat); padding: 8px 13px; border: 2px solid var(--ink); background: transparent; color: var(--ink); white-space: nowrap; cursor: pointer; transition: background-color .15s var(--ease), color .15s var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .basket-rec-btn:hover { background: var(--ink); color: #fff; }
}

/* basket — grey box with yellow (matches the checkout) */
.basket-panel { background: var(--grey) !important; color: var(--yellow); border-bottom-color: var(--yellow); }
.basket-panel .header-1, .basket-item-name, .basket-item-meta, .basket-qty-n, .basket-recs-title, .basket-empty { color: var(--yellow) !important; }
.basket-panel .u { color: var(--yellow); }
/* FULL YELLOW (Cam, 10 Sep: "the rules on the basket should be yellow not lower opacity").
   The rules were at 30%, from when they were the only lines in the panel and a full-strength
   one between every item read as heavy. The checkout beside it is now two ruled grids at full
   strength, so a faded rule here read as a different, weaker system rather than as restraint. */
.basket-item { border-color: var(--yellow); }
.basket-panel .basket-item:last-child { border-bottom: 0; }
.basket-qty-btn, .basket-rec-btn { border-color: var(--yellow); color: var(--yellow); }
@media (hover: hover) and (pointer: fine) {
  .basket-qty-btn:hover, .basket-rec-btn:hover { background: var(--yellow); color: #4a4a4a; }
}
.basket-recs { border-top: 0; margin-top: 1.1rem; }
.basket-panel .button { background: var(--yellow); color: var(--ink); border-color: var(--yellow); }
.basket-panel #basketUndo, .basket-panel #basketClear { color: var(--yellow); }
/* dark contact-page footer: Kitto logo needs to be white */
@media (hover: hover) and (pointer: fine) {
  .basket-qty-btn:hover { background: var(--ink); color: #fff; }
}
.basket-qty-n { min-width: 1.4ch; text-align: center; font-size: var(--fs-body); }
/* Same inset as .co-remove, so the × behaves the same in the basket as at checkout, and
   full yellow at rest for the same reason: the panel is always grey, this is the only way
   to take something back out of the basket, and dimming it until it is pointed at hides it
   completely on a phone, where nothing is ever pointed at. No hover rule for the same
   reason as .co-remove: it is already at full strength.
   The colour has to be set HERE rather than in the panel's own block further up, because
   this declaration comes later and would win. */
/* THE X IS DRAWN, NOT SET (Cam, 10 Sep: "the x needs to be in line with strokes and needs to
   move more left and can be bigger / bolder").
   It was the &times; character at 1.5rem, which is thin at any size, and the only way to
   weight a glyph is a bold font: this site has none, and the Contents label on the legal
   pages was just corrected for exactly that. The same two-stroke SVG .hero-stage-close uses
   gives the weight through stroke-width instead, so it can be as heavy as it needs to be
   without introducing a weight the type system does not have.
   IN LINE WITH THE STROKES. The row is `1fr auto 1.5rem` and this button was 1rem + .5rem of
   padding around a 24px glyph, so it measured about 37px in a 24px track and overflowed it to
   the right, past the end of the row rule above and below it. The right padding goes and the
   track is sized to the content, so the X's own right edge lands on the rule's, which is what
   moves it left. */
.basket-remove {
  color: var(--yellow);
  display: flex; align-items: center; justify-content: flex-end;
  line-height: var(--lh-flat); padding-left: 1rem; padding-right: 0;
  transition: color .2s var(--ease);
}
.basket-remove svg {
  display: block; width: 17px; height: 17px;
  stroke: currentColor; stroke-width: 2.5; fill: none; stroke-linecap: butt;
}
.basket-empty { color: var(--mid-grey); font-size: var(--fs-body); padding: .5rem 0; }
.basket-empty a { color: var(--ink); text-decoration: underline; }
.basket-foot { display: flex; align-items: center; justify-content: flex-start; gap: 1.25rem; margin-top: 1.75rem; flex-wrap: wrap; }
.basket-foot #basketQuote { margin-left: auto; }   /* keep Checkout on the right; Undo + Clear group together on the left */
.basket-foot .u { color: var(--mid-grey); }
.basket-foot .u:hover { color: var(--ink); }
.basket-foot .button { background: transparent; color: var(--yellow); border-color: var(--yellow); }
.basket-foot .button:hover { background: var(--yellow); color: #000; border-color: var(--yellow); }

/* larger address / contact block on the quote form */
/* 18px on 1.5, the same as the KitMapper block's paragraph, because it is the same thing:
   a 24px heading with the sentence that explains it underneath. This used to be --fs-lead
   on 1.55, which out-specified .form-subtitle below and quietly won, so the quote form's
   line was two sizes and a leading step away from its counterpart on the home page.
   IT STILL OUT-SPECIFIES IT, 0,1,1 against 0,1,0, and .form-subtitle wraps in here. So the
   leading token goes on BOTH or the quote intro silently keeps the old value, which is what
   happened first time round on 19 Aug. Same narrow column either way. */
.form-aside p, .form-aside p a { font-size: var(--fs-body); line-height: var(--lh-narrow); }

/* ═══════════════════ SLIDER — full-width, captions overlaid ═══════════════════ */
.slider-viewport { position: relative; overflow: hidden; }
.slider-track { display: flex; transition: transform .6s var(--ease); will-change: transform; }
.slider-slide { position: relative; min-width: 100%; }
/* 16:9, UP FROM 21:9 (Cam, 19 Aug: about 30% taller, to get the full context of the
   images). At the same width that is 31% more height, since 9/16 against 9/21 is 1.31.
   21:9 was taking only the middle two thirds of a 3:2 photograph, which is a crop that
   suits a landscape but throws away the room an installation shot is standing in, and
   these are pictures of work in a space. Desktop only: below 768 the slide is already
   sized off the viewport at 72vh rather than by ratio. */
.slider-slide img { width: 100%; aspect-ratio: 16/9; object-fit: cover; display: block; }
/* The CRT stack runs almost to the bottom edge, so a centred crop cut its lowest screen
   off. Holding the crop low pushes the picture up in the frame and the whole stack is in.
   Still worth keeping at 16:9: the taller frame gives it more room, not all of it. */
.slider-slide img[src*="work-clements-1"] { object-position: center 75%; }
/* The Gasworks photograph is a wide projection band across a pale wall, so at full frame
   it read as a letterbox inside a letterbox, with the wall behaving like white bars top
   and bottom. Scaling it up pushes the projected work out to the edges of the slide, and
   the wall goes with it. 1.35 is the most this file will take before it visibly softens:
   1920px source across a 1512px slide leaves only so much headroom. */
.slider-slide { overflow: hidden; }
.slider-slide img[src*="work-chan-1"] { transform: scale(1.35); transform-origin: center 46%; }
.slider-slide::after { content: ""; position: absolute; inset: auto 0 0 0; height: 45%; background: linear-gradient(transparent, rgba(0,0,0,.55)); z-index: 1; pointer-events: none; }
/* kit-at-work caption: single centred line at the TOP, same treatment as the hero credit */
.slider-cap { position: absolute; left: 0; right: 0; bottom: clamp(1.25rem, 3.5vw, 3rem); z-index: 2; text-align: center; font-size: var(--fs-13); line-height: var(--cap-lh); }
/* A THIN BAR ACROSS THE FOOT OF THE PICTURE (Cam, 24 Aug). It was briefly a shrink-to-fit
   block floating clear of the bottom edge. Full width and sat down on the edge instead: the
   slide is the whole screen here, and a small yellow tab hanging in the middle of it read as
   a label stuck on the photograph rather than as the picture's own caption.
   Thin is the point, so the padding comes down from 10px to 7px and the bar is only as deep
   as the line of type inside it. The credit is set inline as one flowing line, so it stays
   one line at almost every width and the bar stays a bar.
   SCOPED TO THE FOOTER CAROUSEL, not to .slider-cap at large: the journal article pictures
   use the same class and set their own corner, measure and alignment, and a full-width rule
   here would flatten all of them. */
/* NO BAR (Cam, 24 Aug). The full-width yellow strip put a solid band across the foot of
   every photograph, which on a slide that is the whole screen is a lot of the picture given
   over to a credit. Yellow type straight on the image instead: full width so it still
   centres on the picture, and held off the bottom edge rather than sat on it, because
   without a ground of its own type against an edge reads as falling off it. */
.slider-block .slider-cap {
  left: 0; right: 0; bottom: clamp(1rem, 2.5vw, 2rem); transform: none;
  width: auto; max-width: none;
  background: transparent; padding-inline: var(--gut);
  text-align: center;
}
.slider-cap p { display: inline; font-size: var(--fs-13); line-height: var(--cap-lh); margin: 0; }
/* The work's title, italic as it would be set in print. The subs after it, the artist and
   venue and the photographer, stay roman.
   Scoped to the kit-at-work slides, where the title genuinely is its own paragraph. In a
   journal article the whole credit is one line with an <em> around the title, so this rule
   applied there put the artist, the venue and the photographer in italic too. */
.slider-slide .slider-cap p:not(.cap-sub) { font-style: italic; }
.article-body .ph .slider-cap p, .media-full .ph .slider-cap p { font-style: normal; }
.article-body .ph .slider-cap em, .media-full .ph .slider-cap em { font-style: italic; }
.slider-cap .cap-sub::before { content: ", "; }
.slider-credit { position: absolute; right: var(--gut); bottom: 1.4rem; z-index: 2; font-size: var(--fs-15); }
/* JUST THE ARROW, NO BOX (Cam, 23 Aug 2026). These were a translucent black square with a
   white glyph, which put two dark blocks over the photograph they sit on. Transparent ground,
   yellow mark, and hover draws a yellow stroke round the square the arrow already occupies
   rather than filling it. Same treatment as the expanded view's arrows, so the two carousels
   on the site behave alike. */
.slider-arrow {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 3;
  width: 50px; height: 50px; display: flex; align-items: center; justify-content: center;
  background: transparent; color: var(--yellow); font-size: 1.3rem;
  border: 2px solid transparent;
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
.slider-arrow:hover, .slider-arrow:focus-visible {
  background: transparent; color: var(--yellow); border-color: var(--yellow);
}
/* Same geometric arrow as the menu search, rather than the ← and → glyphs, which Gill
   Sans draws soft and at a different weight to everything around them. 2px, down from the
   3px of the nav's plus: that pen is right for a 22px mark and heavy on one this size.
   non-scaling-stroke holds it at exactly 2px whatever the box ends up. */
.slider-arrow svg { display: block; width: 28px; height: 28px; stroke: currentColor;
  stroke-width: 2; fill: none; stroke-linecap: butt; stroke-linejoin: miter;
  vector-effect: non-scaling-stroke; }
.slider-arrow.prev { left: 16px; }
.slider-arrow.next { right: 16px; }

/* ═══════════════════ NEWSLETTER ═══════════════════ */
.newsletter-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 3.5rem 3rem; align-items: start; max-width: 1000px; margin-inline: auto; }

/* THE JOURNAL CARDS ARRIVE ONE AFTER ANOTHER (Cam, 19 Aug).
   They all cross the observer's threshold within a frame of each other, so the four used
   to appear as one block and the movement read as the section switching on rather than as
   pictures coming in. A delay per card breaks that up, and a longer fade makes each one an
   arrival rather than a flicker.
   Two numbers control it. --reveal-step is the gap between cards, --reveal-fade is how long
   each takes. The nth-child list runs past the four that exist so a fifth or sixth card
   would still be staggered rather than dropping back to arriving with the first.
   These override the shorthand on .reveal: that shorthand sets duration AND resets delay to
   zero, so both have to be restated as longhands, and these selectors outrank it 0,2,0. */
/* A heavier step (Cam, 19 Aug). This is now the ONE load-in on the home page: the hero
   tiles had a staggered arrival for a while and it was taking the attention that belongs
   here, so it was taken out again. With nothing competing, the cards can afford to come in
   slowly and clearly separated, .3s apart rather than .16s, which spreads the four over
   most of a second instead of most of half of one. */
.newsletter-grid { --reveal-step: .3s; --reveal-fade: 1.2s; }
.newsletter-grid .news-card { transition-duration: var(--reveal-fade); }
.newsletter-grid .news-card:nth-child(1) { transition-delay: 0s; }
.newsletter-grid .news-card:nth-child(2) { transition-delay: calc(1 * var(--reveal-step)); }
.newsletter-grid .news-card:nth-child(3) { transition-delay: calc(2 * var(--reveal-step)); }
.newsletter-grid .news-card:nth-child(4) { transition-delay: calc(3 * var(--reveal-step)); }
.newsletter-grid .news-card:nth-child(5) { transition-delay: calc(4 * var(--reveal-step)); }
.newsletter-grid .news-card:nth-child(6) { transition-delay: calc(5 * var(--reveal-step)); }
/* No stagger and no fade when motion is reduced: .reveal already resolves to opacity 1 with
   no transition there, but a delay left standing would still hold the card back. */
@media (prefers-reduced-motion: reduce) {
  .newsletter-grid .news-card { transition-delay: 0s !important; transition-duration: 0s; }
}
/* rows of two, terraced — left card tall, right card landscape and dropped, deliberately not a rigid grid */
/* 4:3, not 4:5. The terrace still works off the difference between the two columns, but a
   portrait frame made each of these run the depth of the card's text and then some. */
.newsletter-grid .news-card:nth-child(2n+1) .ph, .newsletter-grid .news-card:nth-child(2n+1) .news-card-media { aspect-ratio: 4/3; }
.newsletter-grid .news-card:nth-child(2n) .ph, .newsletter-grid .news-card:nth-child(2n) .news-card-media { aspect-ratio: 3/2; }
.newsletter-grid .news-card:nth-child(2n) { margin-top: 3.5rem; }
/* Any card can opt into an upright picture. The terrace alternates 4/3 and 3/2 by position,
   which stops the two columns lining up, but every card still reads as landscape; one
   portrait card gives the grid a second proportion to work against. Same specificity as the
   nth-child rules above, so it has to sit AFTER them to win. */
.newsletter-grid .news-card.news-card--tall .ph,
.newsletter-grid .news-card.news-card--tall .news-card-media { aspect-ratio: 4/5; }
/* A GRID ON THE HOME PAGE, NOT A TERRACE (Cam, 24 Aug). Everything above is built to stop
   the two columns lining up: 4/3 on the left, 3/2 on the right, one card upright at 4/5,
   and every even card dropped 3.5rem. Four cards, four different depths, no two tops on a
   line — which reads as tiles arranged on the page rather than as a set of related things.
   Here it is one proportion for every picture and no drop, so the pictures align across
   and the cards below them start on the same line. The rag at the bottom is then genuinely
   the length of each title, which is information, rather than a stagger built in on top of
   it. Even gutters for the same reason: 3.5rem down and 3rem across gave the rows and the
   columns different rhythms, so 3rem both ways lets the grid read as square.
   HOME ONLY. The journal index runs the same class through a two-column `column-count`
   flow, which wants the varied depths, and it already cancels the drop for itself. The
   terrace above is untouched and is what that page still uses. */
body.home .newsletter-grid { gap: 3rem; }
body.home .newsletter-grid .news-card:nth-child(2n+1) .ph,
body.home .newsletter-grid .news-card:nth-child(2n+1) .news-card-media,
body.home .newsletter-grid .news-card:nth-child(2n) .ph,
body.home .newsletter-grid .news-card:nth-child(2n) .news-card-media,
body.home .newsletter-grid .news-card.news-card--tall .ph,
body.home .newsletter-grid .news-card.news-card--tall .news-card-media { aspect-ratio: 4/3; }
body.home .newsletter-grid .news-card:nth-child(2n) { margin-top: 0; }
.news-card { display: flex; flex-direction: column; gap: 0; }
.news-card-media, .news-card .ph { aspect-ratio: 4/3; margin-bottom: 18px; }
.news-card .date { font-size: var(--fs-12); line-height: 1.4; color: var(--ink); }
.page-head .article h1 { margin-bottom: 1.5rem; }   /* space under the article title, above the hero image */
/* HALF a line under the title (Cam, 19 Aug: a full one was too much). Still tied to the
   leading rather than being a loose pixel value, so the break moves with the type if the
   leading ever changes. Covers the home page hero cards and the journal listing alike:
   both are .news-card, so there is one rule, not two to keep in step.
   The title and the standfirst were running together as one block of type before this. */
.news-card .title {
  font-size: var(--fs-body); line-height: var(--lh-narrow); margin-top: 2px;
  margin-bottom: calc(var(--lh-narrow) * .5em);
  font-style: normal; text-decoration: none;
}
/* ITALIC IS THE HOVER, NOT THE RESTING STATE (Cam, 7 Sep 2026).
   These titles used to be italic all the time, so hovering one changed only the underline
   and the italic said nothing. Everywhere else on this site italic IS the hover: the nav,
   the catalogue names, the home category cards. Resting normal and italicising on hover
   puts the news cards back in step with all of them.
   One rule covers the journal listing AND the home page hero cards, since both are
   .news-card, so this moves together on both. */
/* THE PICTURE AND THE TITLE ARE ONE LINK, SO THEY HOVER AS ONE (Cam, 7 Sep 2026:
   "they both pull you to the same place").
   A card holds THREE anchors and every one of them points at the same page: the media
   link round the image, the link round the title, and a 'Read more' this site hides. They
   were separate hover scopes, so crossing the picture tinted it while the title sat there
   unchanged, and the card read as two things rather than one.
   The title's anchor FOLLOWS the media link inside the card, so a sibling selector reaches
   it and no :has() is needed. .ph is the placeholder used by cards with no photograph and
   behaves the same way. */
.news-card a:hover .title,
.news-card a:focus-visible .title,
.news-card .news-card-media:hover ~ a .title,
.news-card .ph:hover ~ a .title { font-style: italic; text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
/* No break between the title and the description. The title now carries the separation on
   its own bottom margin rather than on a contrast of italic against roman; a blank line as
   well would say it twice. */
.news-card p { font-size: var(--fs-body); line-height: var(--lh-narrow); margin-top: 0; }
/* The card's text is centred under its picture (Cam, 19 Aug), the same call as the
   expanded product card. Date, title and standfirst are one stack, so they take the
   alignment together, not line by line. The picture above is untouched. */
.news-card .date, .news-card .title, .news-card p { text-align: center; }
.news-card a.kit-small { display: none; }   /* no 'Read more' — matches the precedent */

.news-feature { display: grid; grid-template-columns: 1.4fr 1fr; gap: clamp(2rem,4vw,4rem); align-items: center; margin-bottom: 4rem; }
.news-feature .ph { aspect-ratio: 3/2; }
.news-feature .date { font-size: var(--fs-15); color: var(--mid-grey); }

/* ═══════════════════ INFO / STATS ═══════════════════ */
.info-block { background: var(--paper); }
/* about: "part of the KitMapper group" — Kitto-yellow band */
.kitmapper-band { background: var(--yellow); color: var(--ink); padding-block: clamp(2.5rem, 6vw, 4.5rem); }
.kitmapper-band .header-1 { margin-bottom: 1.2rem; max-width: 24ch; }
.kitmapper-band p { max-width: 62ch; margin-bottom: 1.6rem; line-height: var(--lh-body); }
.kitmapper-band .button { background: var(--ink); color: #fff; border-color: var(--ink); }
.kitmapper-band .button:hover { background: transparent; color: var(--ink); border-color: var(--ink); }
.info-block-text p { font-size: var(--fs-big); line-height: 1.25; max-width: 20ch; }
.info-stats { display: flex; gap: clamp(2rem, 7vw, 8rem); margin-top: clamp(2.5rem, 5vw, 3.5rem); flex-wrap: wrap; }
.info-stat .num { font-size: clamp(2.2rem, 4.5vw, 3rem); line-height: var(--lh-flat); }
.info-stat .lab { font-size: var(--fs-15); color: var(--mid-grey); margin-top: .5rem; }

/* ═══════════════════ PARTNERSHIP ═══════════════════ */
.partnership-block { background: var(--white); }
.partnership-inner { display: grid; gap: clamp(2rem, 8vw, 7rem); grid-template-columns: 1fr 1.4fr; }
.partnership-item-left > .header-1 { margin-bottom: .9rem; }
.partnership-item-left p { margin-bottom: 1rem; line-height: var(--lh-narrow); }
.partnership-item-left-bottom { margin-top: 1.5rem; display: flex; flex-direction: column; gap: 1.2rem; }
/* THE TWO KNOBS FOR THIS BLOCK, AND THEY ARE THE ONLY TWO.
   row-gap sets the leading between rows of logos at EVERY width, and --logo-scale sets the
   size of every mark at every width. No media query below overrides either: that is the
   point, and it is what stops the block looking different at each breakpoint. Effective
   spacing between rows is row-gap plus the 4px top and bottom on .partner-logo-item.
   COLUMNS ARE `auto`, NOT `1fr`, AND THE GRID PACKS FROM THE LEFT. Equal-width cells made
   the horizontal spacing a function of the mark inside: every logo sat hard left in a cell
   sized for the widest one, so Koppel (50px of ink) carried the same 141px cell as Cromwell
   Place (137px), and the marks drifted apart with nothing between them. At 970 that left
   most of a row as white space. Sized to content instead, column-gap IS the space between
   the marks, one number, the same at every width.
   Used by the homepage block and the about page, which share this markup. */
.partnership-logos {
  display: grid; grid-template-columns: repeat(4, auto); justify-content: start;
  column-gap: 28px; row-gap: 24px; margin-top: .5rem; --logo-scale: 1.18;
}
/* Padding only on the vertical. The horizontal 10px was quietly capping the widest marks:
   these are 1400x1120 canvases, so a mark set to 119px high wants 149px of width, and the
   cell only had 125 to give. The gap between cells does the separating instead, and the
   canvases carry their own white space either side of the ink. */
.partner-logo-item { padding: 4px 0; display: flex; align-items: center; justify-content: flex-start; }
/* EVERY logo aligns left, not just the row leader (19 Aug, Cam). "Learn about KitMapper"
   and the clients label both start hard at the column edge, and centring columns 2 to 4
   gave the block four different left edges per row, which read as ragged against text that
   has one. Both narrower bands already did this, see the 2-across and 3-across blocks
   below, so desktop was the inconsistent one rather than this being a new idea.
   Trailing space no longer varies either: the columns are sized to their content rather
   than to equal fractions, so the gap after each mark is column-gap and nothing else. */
.partner-logo-item span { text-align: center; font-size: var(--fs-12); color: var(--mid-grey); }
/* SIZED BY HEIGHT, ONE NUMBER FOR THE WHOLE SET, AT EVERY WIDTH.
   These used to be sized as a percentage of their grid cell, so the marks were a side
   effect of column width: they grew and shrank with the layout and jumped at every
   breakpoint. Measured on 19 Aug, Cromwell Place ran from 25.0px at 810 to 35.7px at 590,
   a 43% swing, and it went UP each time the screen got narrower, which is backwards.
   The files are cropped to their alpha bounds, so the box IS the mark and the intrinsic
   aspect ratio is the mark's own. That means height alone fixes both dimensions.
   --logo-h is the optical height each mark is tuned to, balanced by ink mass rather than
   by the eye (see below). --logo-scale, set once on .partnership-logos, takes the whole
   set up or down together, so the proportions between them can never drift.
   max-width is a backstop only. If it ever bites, the cell is too narrow and the fix is
   FEWER COLUMNS in that band, not a smaller scale, otherwise the marks start varying with
   the layout again. The column counts below are chosen so it does not bite. */
.partner-logo-item img {
  --logo-h: 27.8px;
  height: calc(var(--logo-h) * var(--logo-scale)); width: auto;
  max-width: 100%;
  object-fit: contain;
}
/* Optical balancing — all logos share a 700×560 canvas but fill it by
   different amounts, so a uniform height makes the heavy ones (180, Koppel)
   loom and the light ones (Cromwell) vanish. Nudge per logo by ink weight. */
/* WHY THESE NUMBERS LOOK ARBITRARY. Every one of these PNGs is a 1400x1120 canvas, and
   the mark inside occupies a different part of it: 180 Strand's ink is 1097x522, Peckham
   24's is 1099x134. So max-height sizes the CANVAS, not the logo, and two logos set to
   the same height are nowhere near the same size on screen. Tuning by eye against the
   canvas is why this drifted.
   Set instead by ink mass, the alpha-covered pixels scaled to render size, so a solid
   mark and a hairline mark carry comparable weight. 180 Strand and Koppel were at roughly
   3x the mass of the lighter marks, which is what made them shout. */
/* The whole row was taken up 20% on 18 Aug. The ratios between them are the balance, so
   they move together: change one and the row goes out of true. */
.partner-logo-item img[src*="blue-marine"]       { --logo-h: 27.8px; }
.partner-logo-item img[src*="ingram-collection"]       { --logo-h: 24.8px; }
.partner-logo-item img[src*="180-studios"]       { --logo-h: 27.9px; }  /* stacked + solid, mass 1313 → ~800 */
.partner-logo-item img[src*="koppel"]            { --logo-h: 42.6px; }  /* solid black disc, mass 1458 → ~800 */
.partner-logo-item img[src*="dazed"]             { --logo-h: 18.5px; }  /* heavy bold wordmark, 984 → ~800 */
.partner-logo-item img[src*="peckham-market"]    { --logo-h: 14.9px; }
.partner-logo-item img[src*="peckham-24"]        { --logo-h: 11.9px; } /* light single line, lift */
.partner-logo-item img[src*="cromwell-place"]    { --logo-h: 25.5px; } /* thin hairline, was getting lost */
/* No space above it. The label belongs to the logos underneath, not between them and the
   KitMapper link, and the 1.4rem that used to sit here left it floating in the middle with
   nothing to attach to. Closed up, it reads as the heading of the row it introduces. */
/* Both set to --fs-body so the right column reads at the same size as the paragraph on the
   left (Cam, 19 Aug). The label was 12px, a caption size, which made the column look like
   footnotes next to a body-size claim. */
.clients-label { font-size: var(--fs-body); color: var(--mid-grey); margin: 0 0 .6rem; }
.partnership-item-right > .u { font-size: var(--fs-body); }
.partnership-item-right p { font-size: var(--fs-body); line-height: var(--lh-body); }

/* ═══════════════════ CTA BAND ═══════════════════ */
.cta-band { background: var(--yellow); }
.cta-band .container { display: flex; align-items: center; justify-content: space-between; gap: 2rem; flex-wrap: wrap; }
.cta-band .header-1 { max-width: 22ch; }

/* ═══════════════════ FORM ═══════════════════ */
.form-block { padding-block: var(--sec); }   /* own spacing — injected form-blocks live outside <main> */
/* GREY GROUND, YELLOW TYPE (Cam, 10 Sep: "Request a quote block should be grey with yellow
   text on top in all instances"). It was `.black`, white on near-black, and it was the last
   panel on the site still doing that: the basket, the checkout and the catalogue are all
   yellow on Kitto grey, and this is the fourth panel of the same kind, a form dropped over
   the page. The class name changes with the colours rather than lying about them. */
.form-block.grey {
  background: var(--grey); color: var(--yellow);
  /* A YELLOW KEYLINE ON THE GREY, as every grey panel on the site carries (Cam, 10 Sep:
     "yellow stroke on the grey on the block itself, as always"). Top and bottom only: the
     band is full bleed, so those are its two real edges. The bottom one meets the yellow
     footer and disappears into it, which is correct rather than wasteful, the block does not
     know what follows it. */
  border-top: 2px solid var(--yellow);
  border-bottom: 2px solid var(--yellow);
}
.form-block.grey .header-1, .form-block.grey p, .form-block.grey a, .form-block.grey label, .form-block.grey .form-subtitle { color: var(--yellow); }
.form-inner-wrap { display: grid; grid-template-columns: 1fr 1.3fr; column-gap: 5rem; row-gap: 50px; }
.form-aside { display: flex; flex-direction: column; gap: 2rem; justify-content: space-between; }
/* Body size, not --fs-15. This is the same heading-then-paragraph pair as the
   KitMapper block, so it should carry the same weight; at --fs-15 it read as a
   caption under the title rather than the sentence that explains it. */
/* MATCHED TO THE KITMAPPER BLOCK, deliberately. Both are the same thing: a 24px heading
   with the sentence that explains it underneath. So both are 18px on 1.5, and neither is
   allowed to drift on its own. If this pairing changes, change it in both places. */
.form-subtitle { font-size: var(--fs-body); color: var(--on-dark); display: block; line-height: var(--lh-narrow); }
.form-aside .header-1 { display: block; margin-bottom: 8px; }
.form-inner { display: grid; grid-template-columns: 1fr 1fr; column-gap: 60px; row-gap: 14px; align-items: start; }
.form-item { display: flex; flex-direction: column; }
.form-item.full { grid-column: 1 / -1; }
/* Grid items default to min-width:auto, so a field refuses to shrink below its content
   and pushes the row past the right edge on a narrow screen. */
.form-item { min-width: 0; }
.form-item input, .form-item select, .form-item textarea { max-width: 100%; }
label { font-size: var(--fs-13) !important; margin-bottom: 9px; display: block; color: var(--on-dark); }
input, textarea, select {
  height: 46px; background: var(--ink); color: #fff; width: 100%;
  /* NO VERTICAL PADDING on the single-line fields. With box-sizing: border-box a 46px
     box, less 4px of border and 26px of padding, left exactly 16px of content box for
     16px text, so the glyphs were clipped top and bottom. It looked fine until a value
     was chosen, because "Select…" has no descenders while "Independent artist" and
     "No, dry hire" do. The fixed height centres the text on its own. */
  /* ⚠️ font-size STAYS AT 16px AND IS DELIBERATELY OFF THE TYPE SCALE.
     iOS Safari zooms the viewport whenever a form field smaller than 16px takes focus,
     and it does not zoom back out. Dropping this to --fs-15 to match the rest of the
     small UI would look tidier in the stylesheet and break every form on an iPhone.
     Checked 10 Sep 2026 during the scale tidy-up. Do not "fix" this. */
  padding: 0 13px; line-height: var(--lh-tight); font-size: 1rem;
  border: 2px solid #fff; border-radius: 0;
}
textarea { height: 116px; resize: none; padding: 13px; line-height: var(--lh-body); }
select { appearance: none; cursor: pointer; background-image: linear-gradient(45deg,transparent 50%,#fff 50%),linear-gradient(135deg,#fff 50%,transparent 50%); background-position: calc(100% - 18px) 19px, calc(100% - 12px) 19px; background-size: 6px 6px,6px 6px; background-repeat: no-repeat;
  /* Clear the arrow, and fail to an ellipsis rather than running underneath it. */
  padding-right: 34px; text-overflow: ellipsis; }
select option { background: var(--ink); color: #fff; }
input::placeholder, textarea::placeholder { color: rgba(255,255,255,.5); }
input:focus, textarea:focus, select:focus { outline: 2px solid #fff; }
input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1) brightness(2); cursor: pointer; }
.form-block .submit-btn { width: auto; min-width: 0; align-self: flex-start; }
/* Fills yellow and reverses to the panel's own grey, which is exactly what the buttons in
   the basket do. --ink would be a third colour on a two-colour panel. */
.form-block.grey .submit-btn { background: transparent; color: var(--yellow); border-color: var(--yellow); }
.form-block.grey .submit-btn:hover, .form-block.grey .submit-btn:focus-visible { background: var(--yellow); color: var(--grey); border-color: var(--yellow); }

/* ═══════════════════ PAGE BANNER ═══════════════════ */
.page-banner { background: var(--yellow); padding-block: clamp(40px, 5vw, 70px); }
.page-banner h1 { font-size: var(--fs-banner); font-weight: 400; line-height: var(--lh-head); max-width: 18ch; }
.page-banner p.lead { margin-top: 1.2rem; max-width: 56ch; }
.breadcrumbs { color: var(--mid-grey); margin-bottom: 1.2rem; display: flex; gap: .5rem; }
.breadcrumbs, .breadcrumbs a, .breadcrumbs span,
body[data-page="equipment"] .breadcrumbs, body[data-page="newsletter"] .breadcrumbs { font-size: var(--fs-12); }
.breadcrumbs a:hover { color: var(--ink); }
/* The page you are ON is italic (Cam, 19 Aug), everywhere on the site. It is the one crumb
   that is not a link, and until now it was indistinguishable from the separators and the
   links beside it. Last child rather than a class: the trail always ends with the current
   page in a span, checked across all 19 pages, and the separators are spans too, so
   :last-child is what picks it out without touching the markup. */
.breadcrumbs span:last-child { font-style: italic; }
/* Black on the yellow pages. Mid-grey is a quiet colour on white and a muddy one on yellow,
   with too little between the two to read as type at 12px. */

/* plain page header — title + secondary at the SAME size; hierarchy from colour */
.page-head { padding-block: clamp(40px, 5vw, 64px) 0; }
/* page titles adopt the article hierarchy: large display h1, quieter sub below */
.page-head h1 { font-size: var(--fs-banner); font-weight: 400; line-height: var(--lh-head); color: var(--ink); max-width: 20ch; }

/* THE PAGE HEAD IS CENTRED ON THE EQUIPMENT AND JOURNAL PAGES (Cam, 19 Aug).
   Scoped by data-page, which means the kit category pages come with equipment.html and the
   articles come with journal.html. That is deliberate: those pages share the head's markup
   exactly, and centring the index while its own children stayed left would read as a
   mistake rather than as a distinction. About, rental agreement and request a quote are
   untouched and still lead from the left.
   h1 and .sub both carry a max-width, so they need margin-inline to centre the BLOCK as
   well as text-align to centre the lines inside it. The breadcrumbs are a flex row, where
   text-align does nothing, so they take justify-content instead. */
body[data-page="equipment"] .page-head .container,
body[data-page="journal"] .page-head .container,
body[data-page="thanks"] .page-head .container { text-align: center; }
body[data-page="equipment"] .page-head .breadcrumbs,
body[data-page="journal"] .page-head .breadcrumbs { justify-content: center; }
body[data-page="equipment"] .page-head h1,
body[data-page="journal"] .page-head h1,
body[data-page="thanks"] .page-head h1,
body[data-page="equipment"] .page-head .sub,
body[data-page="journal"] .page-head .sub,
body[data-page="thanks"] .page-head .sub { margin-inline: auto; }
/* BALANCED RAG (Cam, 19 Aug). Centred text is where an uneven rag shows worst: a long
   first line over a short second one reads as a mistake when both are centred on the same
   axis, and .sub is capped at 56ch so it wraps on most widths. text-wrap: balance evens the
   line lengths across the block instead of filling each line before moving on.
   It applies to the heading too, which is capped at 20ch and wraps on the longer titles.
   Browsers stop balancing past a handful of lines, which is why this is right for a
   standfirst and would be the wrong tool for body copy. */
body[data-page="equipment"] .page-head h1,
body[data-page="journal"] .page-head h1,
body[data-page="thanks"] .page-head h1,
body[data-page="equipment"] .page-head .sub,
body[data-page="journal"] .page-head .sub,
body[data-page="thanks"] .page-head .sub { text-wrap: balance; }

/* WIDER NEWS HEADS (Cam, 24 Aug). The article titles were running tight: the Koppel open
   call is 73 characters, which at the inherited 20ch cap cannot fit in fewer than four
   lines, so `balance` was evening out four short lines rather than making a bad rag.
   The cap is the constraint, not the balancing, so the cap moves and the balancing stays.
   30ch gives the long titles three lines and the short ones one or two, while still
   holding a measure: without a cap a centred display line would run the full container
   and lose the shape the centred head depends on. Scoped to news only, so About, the
   rental agreement and the rest keep the tighter 20ch they were set with. */
body[data-page="journal"] .page-head h1 { max-width: 30ch; }

/* ═══ JOURNAL INDEX: THE HEAD IS THE FIRST CELL OF THE GRID (Cam, 19 Aug) ═══
   The trail sits in its own cell on top of the title and standfirst, the two sharing one
   stroke so they read as one object divided rather than as two boxes near each other. One
   border on the card with a rule under the breadcrumbs is what guarantees the two cells are
   the same width: they are the same box.
   It lives INSIDE .newsletter-grid rather than in a .page-head above it, which is what lets
   the cards flow around and underneath it. A grid container will not wrap around a floated
   box, so being a grid item is the only way to get this; sitting in its own section, the
   head could only ever be a full-width band with the cards starting below it.
   White card on the grey with the same lift the hero tiles and the thumbnails take,
   0 6px 22px at .34. Anything lighter disappears against that background. The stroke drops
   to --line and does the dividing only: with a white fill and a real shadow the box already
   has an edge, and a heavy rule would be a third thing separating it from the page. */
.journal-head-card {
  align-self: start;
  max-width: 400px;
  margin-right: auto;                  /* held to the left of its column */
}
/* THE HIRE PAGE HEAD, THE SAME MOVE (Cam, 24 Aug). "Hire" and its standfirst are the first
   cell of .kit-grid now instead of a full-width .page-head band above it, so the categories
   start at the top of the page beside the words rather than a screen below them.
   Built from .journal-head-card's rules, including its centring. It was briefly set left,
   on the reasoning that the kit cards below it are flush left and a head should match its
   cards. That was the wrong pattern to follow: the head is not a card, it is the page
   title, and it reads as the thing the grid belongs to only while it holds its own axis.
   Centred at every width, as the news head is.
   The heading has to state its own size for the same reason the news one does: headings
   are reset to font-size: inherit near the top of the file and take their size from
   context, so the moment this stopped living in .page-head it dropped to body size. */
.kit-head-card {
  align-self: start;
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
}
.kit-head-card h1 {
  font-size: var(--fs-banner); font-weight: 400; line-height: var(--lh-head);
  padding: 0; margin: 0; max-width: none; text-align: center; text-wrap: balance;
}
.kit-head-card .sub {
  font-size: var(--fs-body); line-height: var(--lh-narrow);
  padding: 0; margin: .7rem 0 0; max-width: none; text-align: center; text-wrap: balance;
}
/* Sits under the standfirst on the same centre line, so the three parts of the head read
   as one stack rather than a title with a control parked beside it. */
.kit-head-card .button { margin-top: 1.4rem; }
/* NO BOX AND NO SHADOW (Cam, 19 Aug). The panel was scaffolding: it was doing the work of
   telling the head apart from the cards while the head had no hierarchy of its own to do
   it. With the title back at banner size against a 12px trail and an 18px standfirst, the
   type separates itself and a stroke around it is one device too many.
   The padding goes with it. Inset from an edge that is no longer drawn is just an
   inconsistent margin, so the spacing between the three lines is stated directly instead. */
.journal-head-card .breadcrumbs {
  justify-content: center;
  margin: 0 0 .7rem;
  padding: 0;
}
/* THE HIERARCHY IS RESTATED HERE, because moving the head out of .page-head lost it.
   Line 150 resets every heading to font-size: inherit, so headings on this site are sized
   entirely by context; the size used to come from `.page-head h1`, and the moment the card
   became a grid cell instead, the title dropped to body size. It carries its own scale
   now: 12px trail, banner title, 18px standfirst. */
.journal-head-card h1 {
  font-size: var(--fs-banner); font-weight: 400; line-height: var(--lh-head);
  padding: 0; max-width: none; text-align: center; text-wrap: balance;
}
.journal-head-card .sub {
  font-size: var(--fs-body); line-height: var(--lh-narrow);
  padding: 0; margin-top: .7rem; max-width: none; text-align: center; text-wrap: balance;
}
.journal-head-card .breadcrumbs,
.journal-head-card .breadcrumbs a,
.journal-head-card .breadcrumbs span { font-size: var(--fs-12); }

/* The wrapper around the four cards is INVISIBLE to the grid. display: contents lifts its
   children up so they are grid items beside the head card, exactly as they were before the
   wrapper existed. It only becomes a real box in the narrow band below, where it turns into
   the swipe strip and the head card stays out of it.
   The cards' :nth-child rules are unaffected either way: they count position among their
   own siblings, which is still 1 to 4 inside this wrapper, so the terrace offset and the
   alternating aspect ratios keep working and the head card cannot shift them. */
.newsletter-strip { display: contents; }

/* ═══ THE JOURNAL INDEX FLOWS, IT DOES NOT SIT IN ROWS (Cam, 19 Aug) ═══
   A grid puts every item in row 2 below the TALLEST item in row 1. The head card is about
   250px and a journal card is about 500, so the card underneath the head could not start
   until the card beside it had finished, and there was a column of empty grey between them
   the height of the difference. Rows are the wrong model when the items are not the same
   height.
   Multi-column is, because it is a flow: an item starts where the one above it ended. The
   card below the head now sits directly under it.
   break-inside: avoid keeps a card whole rather than splitting it across the fold, and the
   terrace offset switches off here because it exists to stagger two grid columns and has
   nothing to stagger in a flow.
   SCOPED TO THE JOURNAL PAGE. .newsletter-grid is the home page's journal block too, and
   that one is a deliberate two-column terrace which should stay exactly as it is.
   Above 768 only, so the swipe strip below that width is untouched. */
@media (min-width: 769px) {
  body[data-page="journal"] .newsletter-grid {
    display: block; column-count: 2; column-gap: 3rem;
  }
  body[data-page="journal"] .newsletter-grid > *,
  body[data-page="journal"] .newsletter-strip > * {
    break-inside: avoid; margin-bottom: 3.5rem;
  }
  body[data-page="journal"] .newsletter-grid .news-card:nth-child(2n) { margin-top: 0; }
  /* More air under the standfirst than between the cards themselves (Cam, 19 Aug). The
     head was on 2rem while the cards flow at 3.5rem, so the picture below it sat CLOSER
     than the pictures sit to each other and the head read as the first item in the column
     rather than as the thing introducing it. 4.5rem puts it clearly outside that rhythm. */
  body[data-page="journal"] .journal-head-card { margin-bottom: 4.5rem; }
}
.page-head .sub { font-size: var(--fs-body); line-height: var(--lh-body); color: var(--mid-grey); max-width: 56ch; margin-top: .9rem; }

/* ═══════════════════ ABOUT ═══════════════════ */
.about-intro { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem,5vw,5rem); align-items: start; }
.about-intro .lead { font-size: var(--fs-big); line-height: 1.25; }
/* about lead statement — full-width big type at the existing display increment */
.about-lead { padding-block: clamp(1rem,3vw,2rem) clamp(2rem,5vw,3.5rem); }
.about-lead p { font-size: var(--fs-display); line-height: var(--lh-head); margin-bottom: 1.4rem; }
.about-lead p:last-child { margin-bottom: 0; }


/* PREVIOUS CLIENTS, a list rather than eight logos. Ruled rows in two columns, filled down
   then across, so the names stay alphabetical reading down each column.
   Set on the body size rather than the catalogue's 12px (Cam, 23 Aug 2026): the catalogue is
   a dense index you scan for one item, this is eight names you read, and at 12px on a page
   whose other text is 18px it read as a footnote. */
.about-clients { padding-block: clamp(1.5rem, 4vw, 3rem) clamp(2rem, 5vw, 3.5rem); }
/* Subhead over the list, at the lead size so it belongs to the same voice as "Contact us:"
   and "Located at:" above it rather than introducing a third size to the page. */
/* Centred over the grid, and the grid centred under it, so the two share a middle. Centring
   only the label would have hung it off-axis above a left-aligned block. */
/* LEFT, AND DOWN TO BODY SIZE (Cam, 24 Aug). It was centred at the lead size over a
   centred sheet, which made the block the one thing on the page with its own axis. It
   starts on the same left edge as everything else now, at the size of the writing above it,
   so it introduces the list rather than announcing it. */
/* THE SIZE OF EVERYTHING ELSE ON THE PAGE (Cam, 24 Aug). This has been the wrong way round
   twice. It was --fs-lead, and I read "the same size as the rest of the text" as the page's
   BODY size and took it down to --fs-body, which made it smaller than what it was already
   smaller than. The about page is set at --fs-display throughout: the lead statement and
   the contact block are both on it, and they are the whole page. The client list is on it
   now too, so the three blocks are one voice and the names are not a footnote to them.
   Left aligned, as before. */
.about-clients-label {
  font-size: var(--fs-display); line-height: var(--lh-head);
  margin-bottom: clamp(1rem, 2.5vw, 1.75rem);
  text-align: left;
  /* Italic, which is what separates the label from the names now that it is the same size
     as them. It was doing that with size before and cannot any more, and a heavier weight is
     not available: the licence is Regular only, which is why the site marks emphasis with
     italic everywhere it needs it. */
  font-style: italic;
}
/* A GRID, NOT FLOWED COLUMNS. `columns: 2` let each column find its own rhythm, so the rules
   under the names in the left column never met the ones on the right and the block read as
   two lists side by side. A grid puts every name on a shared row, so the rules line up across
   and it reads as one table.
   grid-auto-flow: column with the row count stated keeps the names alphabetical DOWN each
   column, which is how a list of names is read; the default row flow would run them across
   and put B next to A. The row count is half the list, so it needs bumping when the list
   grows past an even split. */
/* RULED ON ALL FOUR SIDES, like a sheet. No gap between the cells and no doubled lines: the
   container draws the top and left edges, every cell draws its own right and bottom, so each
   line in the grid is one 2px rule and not two sitting on each other. */
.about-clients-list {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-auto-flow: column;
  grid-template-rows: repeat(11, auto);      /* 22 clients, 11 a column */
  grid-template-columns: 1fr 1fr;
  gap: 0;
  /* The measure goes back up with the type. At display size "Anglo-Japanese Foundation" is
     most of a column on its own, so the sheet takes the container's full width rather than
     a cap chosen for smaller type; margin-inline: 0 keeps it on the left edge. */
  max-width: none;
  margin-inline: 0;
  /* currentColor is the yellow the grey page paints all its text, so the sheet is drawn in
     the same stroke as everything around it and needs no colour of its own. */
  border-top: 2px solid currentColor;
  border-left: 2px solid currentColor;
}
/* TWO COLUMNS ONLY WHEN THERE IS ROOM FOR TWO. Below this the sheet is folded into a
   single column and the names run straight down it: two columns of names on a narrow
   screen gives each one about half a phone to sit in, and the longer client names wrap
   inside their own cell. The breakpoint is up from 600 because the type came down to body
   size, so the fold happens where the names actually stop fitting rather than at a width
   chosen for the larger setting. */
/* TWO COLUMNS FOR AS LONG AS TWO WILL FIT (Cam, 24 Aug). The fold moves with the type: at
   display size a column needs roughly 450px for the longest name, so two of them want about
   900 and below that the second column only forces every long name to wrap inside its own
   cell. One column from here down. */
@media (max-width: 900px) {
  .about-clients-list { grid-auto-flow: row; grid-template-rows: none; grid-template-columns: 1fr; }
}
.about-clients-list li {
  /* Same size as the "We've worked with:" head above, so the block is one voice rather than
     a heading over a smaller list. */
  /* Display size, matching the label above and the lead and contact blocks above that.
     line-height 1.15 rather than a body leading: this is display type now, and --lh-narrow
     at 36px would open gaps between the rules of the sheet.
     Padding in em, so it scales with the type and the cells keep their proportions. */
  font-size: var(--fs-display); line-height: var(--lh-head);
  padding: .35em .5em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
}

/* Contact and address on the about page, at the lead size in two columns (Cam, 23 Aug 2026).
   Same size, leading and tracking as .about-lead deliberately: it is the end of the same
   statement, not a separate block, and giving it its own size would break that. Stacks to
   one column below 768, where two columns of 48px type have nowhere to go. */
.about-contact { padding-block: 0 clamp(2rem, 6vw, 4rem); }
.about-contact-grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.5rem, 4vw, 3rem); }
.about-contact p { font-size: var(--fs-display); line-height: var(--lh-head); }
.about-contact p + p { margin-top: .35em; }
@media (max-width: 768px) {
  .about-contact-grid { grid-template-columns: 1fr; gap: 2rem; }
}
/* ONE STEP DOWN THE SCALE ON A PHONE (Cam, 24 Aug). Both of these are set at --fs-display,
   which bottoms out at 36px, and at 317px wide that is four or five words to a line and a
   paragraph that runs most of the screen. --fs-statement is the next increment down, 27px
   at this width, which holds the same relationship to everything around it, just smaller.
   The two are set together on purpose: the contact block is written as the end of the same
   statement and shares the lead's size, leading and tracking, so it has to move with it. */
@media (max-width: 576px) {
  /* The client list steps down with them: it is set at the same size as the lead now, so it
     has to take the same reduction or it becomes the largest type on the phone. */
  .about-lead p, .about-contact p,
  .about-clients-label, .about-clients-list li { font-size: var(--fs-statement); }
}
.team-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 2rem; }
.team-card .ph { aspect-ratio: 4/5; margin-bottom: 1rem; }
/* Yellow cells with the grid drawn across them in black. The rules are the container showing
   through the gaps rather than borders on the cells, which is what keeps them a single
   unbroken 2px line all the way across instead of doubling up where two cells meet.
   2px to match every other stroke on the site. On the about page the ground is yellow too, so
   the block is held together by the black grid alone, which is the point of it. */
/* Slight drop shadow, black. The block is white cards on a white page held together by ink
   hairlines, so it had no edge of its own; this lifts it off the page without adding another
   rule around it. Kept soft and close, the same weight the hero tiles cast. */
.value-grid {
  display: grid; grid-template-columns: repeat(3,1fr); gap: 2px;
  background: var(--ink); border: 2px solid var(--ink);
  /* SHADOW REMOVED (Cam, 24 Aug): was `box-shadow: 0 6px 18px rgba(20,20,20,.18)`. */
}
/* White, not yellow. These sit under "What you can count on" on the about page, which is a
   white page now, and three yellow panels on it were the last of the old ground showing
   through. The grid keeps its ink background and 2px gap, so the divisions still read as
   hairlines between the cards rather than as boxes. */
.value-grid > div { background: var(--white); color: var(--ink); padding: clamp(1.6rem,3vw,2.4rem); }
/* Everything in a cell is black. The numbers used to be mid-grey, which worked as a quiet
   mark on white but goes muddy on yellow, and there is nothing to gain by half-hiding a
   two-digit number. Their size is what keeps them quiet. */
.value-grid .n { font-size: var(--fs-15); color: var(--ink); margin-bottom: 1rem; }
.value-grid h3 { font-size: var(--fs-25); margin-bottom: .6rem; }
.value-grid h3, .value-grid p { color: var(--ink); }
.value-grid p { line-height: var(--lh-narrow); }

/* ═══════════════════ ARTICLE ═══════════════════ */
.article { max-width: 760px; margin-inline: auto; }
.article .meta { font-size: var(--fs-15); color: var(--mid-grey); margin-bottom: 1rem; }
.article h1 { font-size: var(--fs-banner); font-weight: 400; line-height: var(--lh-head); margin-bottom: 1rem; }
.article-body p { font-size: var(--fs-body); line-height: var(--lh-body); margin-bottom: 1.5rem; }
/* search collapses while the basket is open, so the basket sits flush to the nav */
body.basket-open .kit-search.in-nav { display: none; }
.article-body h2 { font-size: var(--fs-25); margin-block: 2.2rem 1rem; }
/* position: relative, or the overlaid credit inside these frames has nothing to anchor to
   and flies up the page to the nearest positioned ancestor. That is what broke the
   captions on the journal articles: all three landed on top of each other near the top of
   the page instead of sitting on their own picture. */
.article-body .ph, .media-full .ph { position: relative; }
.article-body .ph { aspect-ratio: 16/9; margin-block: 2rem; }
/* PORTRAIT PICTURES KEEP THEIR SHAPE (Cam, 10 Sep 2026: the Sophie Clements frame was
   "cropping a big part of the image"). The rule above forces every article picture into
   16/9 and .ph img is object-fit: cover, so an upright photograph loses everything above
   and below a horizontal band through its middle. work-clements-1 is 1499x2000, so about
   half the picture was being thrown away.
   3/4 rather than each image's exact ratio: it is within a percent of the Clements frame
   and close enough for the Lazovy pair, and one shape keeps the article rhythm regular.
   The caption still overlays correctly, it is positioned against .ph either way.
   Add ph--portrait to any upright picture; landscape ones need nothing. */
.article-body .ph.ph--portrait { aspect-ratio: 3/4; max-width: 34rem; margin-inline: auto; }
/* The credit as a label stuck to the picture, the same yellow block with black type the hero
   tiles carry, and in the same corner. These pictures sit in a column of text at a fraction
   of the width of a full-bleed slide, and the overlaid treatment, yellow type with a stroke
   and a drop shadow, was made to hold a line across a picture the width of the screen. At
   this size it was doing far more work than the picture needed, and a line centred across a
   small photograph reads as a title on it rather than a credit beside it.
   Undoing the stroke and the shadow is deliberate: the shared overlaid-caption rule applies
   them to every .slider-cap, and on a solid yellow block behind black type there is nothing
   for them to do but roughen the letters. */
.article-body .ph .slider-cap, .media-full .ph .slider-cap,
.article-body .ph .slider-cap .cap-sub, .media-full .ph .slider-cap .cap-sub {
  color: var(--ink); -webkit-text-stroke: 0; text-shadow: none;
}
.article-body .ph .slider-cap, .media-full .ph .slider-cap {
  top: 0; right: 0; bottom: auto; left: auto;
  max-width: min(70%, 40ch);
  background: var(--yellow);
  padding: 10px 12px;
  /* pretty, not balance, for the reason given on .hero-tile-cap span: balance shortens the
     lines without narrowing the block, and right-aligned type pools that slack on the left
     as empty yellow. These are meant to match the hero tiles, so they wrap the same way. */
  text-align: right; text-wrap: pretty;
  /* Declared, not inherited. The credit is one paragraph set inline, so the height of its
     lines comes from the strut of this block rather than from the paragraph, and this block
     was picking up the article's 16px/1.5. That is where the 24px came from, and why the
     tiles and these never matched: nothing was setting it on purpose. Both now read the
     same token, so the two cannot drift again. */
  font-size: var(--fs-13); line-height: var(--cap-lh);
}
/* The colour has to be set on the paragraph itself, not left to inherit from the block: the
   journal pages are grey-page, and `body.grey-page main p` paints every paragraph in the
   article yellow, which beats anything inherited and left black type invisible on the block. */
.article-body .ph .slider-cap p, .media-full .ph .slider-cap p {
  color: var(--ink);
  font-size: var(--fs-13); line-height: var(--cap-lh);
}
/* The block shrink-wraps to its max-content width, but `balance` then sets the lines
   SHORTER than that. The difference is dead yellow, and because the type is right aligned it
   all pools on the left. Narrowing the cap per breakpoint leaves balance less room to give
   back, and under 480px the rag is dropped for `pretty`, which fills each line to the edge
   and leaves only the last one short. */
@media (max-width: 768px) {
  .article-body .ph .slider-cap, .media-full .ph .slider-cap { max-width: min(82%, 30ch); }
}
@media (max-width: 480px) {
  .article-body .ph .slider-cap, .media-full .ph .slider-cap {
    max-width: min(92%, 26ch); text-wrap: pretty;
  }
}
/* A BAND ALONG THE BOTTOM, AS ON THE HOME PAGE (Cam, 24 Aug). Same change and same reason
   as .hero-tile-cap: a shrink-to-fit block in the top right cuts a ragged step out of the
   corner of the picture, and on a phone that corner is a large share of what there is to
   see. Full width along the bottom instead, so the whole of the top of the photograph is
   clear and every picture in the article carries the same shape.
   The max-widths above are what held it off the edges, so they go with it; without that
   the block keeps sizing to its content and never reaches both sides. */
@media (max-width: 768px) {
  .article-body .ph .slider-cap, .media-full .ph .slider-cap {
    top: auto; bottom: 0; left: 0; right: 0;
    max-width: none;
    text-align: left;
    padding: 8px 12px;
  }
}
/* Upright pictures in an article. `.article-body .ph` forces 16/9, which is right for the
   landscape documentation every piece has carried so far, but crops a portrait picture to a
   slot. Both of these opt out and take 4/5 instead: .ph-tall for one picture, held to the
   width of a column so it does not tower over the text, .ph-pair for two side by side. */
.article-body .ph.ph-tall { aspect-ratio: 4/5; max-width: 480px; margin-inline: auto; }
.article-body .ph-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 1.2rem; margin-block: 2rem; }
.article-body .ph-pair .ph { aspect-ratio: 4/5; margin-block: 0; }
@media (max-width: 576px) {
  .article-body .ph-pair { grid-template-columns: 1fr; gap: 1.6rem; }
  .article-body .ph.ph-tall { max-width: none; }
}
/* Yellow stroke on every picture in a journal article, matching the hero tiles and cards */
.article-body .ph, .media-full .ph { outline: 2px solid var(--yellow); outline-offset: -2px; }

/* ═══════════════ JOURNAL PICTURE, EXPANDED ═══════════════
   A journal picture opens full bleed over the site on yellow (Cam, 19 Aug). The article
   crops to 16:9 and lays the credit over the image, which is right in the flow of a piece
   of writing and wrong for looking at the photograph itself, so expanding shows the whole
   frame uncropped with nothing on top of it.
   The button is .hero-expand's, not a copy of it: same class, so the two places a picture
   opens on this site look and behave identically, and there is one rule to change. */
.article-body .ph .hero-expand, .media-full .ph .hero-expand { position: absolute; }
.article-body .ph:hover .hero-expand, .media-full .ph:hover .hero-expand,
.article-body .ph:focus-within .hero-expand, .media-full .ph:focus-within .hero-expand {
  opacity: 1; transform: none;
}

/* Full bleed, over everything, yellow. Not a scrim over the page: the picture gets the
   whole window and the site goes away, which is the point of expanding it. */
.ph-viewer {
  position: fixed; inset: 0; z-index: 300;
  background: var(--yellow);
  display: flex; flex-direction: column;
  opacity: 0; visibility: hidden;
  transition: opacity .28s var(--ease), visibility .28s;
}
.ph-viewer.open { opacity: 1; visibility: visible; }
/* .ph-viewer sets display:flex and matches [hidden] at the same specificity, so without
   this the hidden attribute loses on source order and the viewer is laid out from load. */
.ph-viewer[hidden] { display: none; }
/* THE PICTURE TAKES THE WHOLE WINDOW (Cam, 19 Aug: make it much bigger).
   No padding and no reserved strip for the caption: the stage is the viewport, and the
   picture is drawn as large as it can be inside it. Two earlier goes both spent screen on
   furniture, first a caption pinned to the bottom edge, then the pair centred as a group,
   and in each the picture ended up smaller than the window it had taken over.
   contain, not cover, so it is still the whole uncropped frame, which is the reason for
   opening it at all. Contain simply has the entire viewport to work with now. */
.ph-viewer-stage {
  flex: 1; min-height: 0;
  display: flex; align-items: center; justify-content: center;
  /* Off the edge, but only just: at zero the picture ran into the top of the window and
     under the close control. This is a third of the padding it started with, so the
     picture is still far larger than the first pass and no longer touching anything. */
  padding: clamp(1rem, 2.5vw, 2rem);
}
.ph-viewer-stage img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; }

/* THE CAPTION IS THE FOOTER (Cam, 19 Aug). Same band as .footer-spacer: same min-height,
   same gutter padding, same 12px ink on yellow, same wrap-to-centre below 576. It reads as
   the same furniture because it is, rather than as a caption invented for this one view. */
/* The caption lies OVER the picture as a band across the foot of the screen, which is
   exactly what the site footer is, so it is built the same way: same min-height, same
   gutter padding, same 12px ink on yellow. Overlaying it rather than stacking it below is
   what lets the picture have the full window. */
.ph-viewer-cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  background: var(--yellow);
  min-height: clamp(2.5rem, 5vw, 4rem);
  display: flex; align-items: center; justify-content: center; text-align: center;
  gap: .4rem 1.5rem; flex-wrap: wrap;
  padding: .9rem var(--gut);
  font-size: var(--fs-12); color: var(--ink);
}
.ph-viewer-cap p { font-size: var(--fs-12); line-height: var(--lh-narrow); margin: 0; }
/* .ph-viewer-cap declares display:flex and so matches [hidden] at equal specificity, the
   same trap as .ph-viewer itself. Without this the band stays laid out when emptied. */
.ph-viewer-cap[hidden] { display: none; }
/* A SQUARE WITH THE GLYPH CENTRED IN IT, AND A BOX ON HOVER (Cam, 19 Aug).
   It was padding .6rem 1rem on a bare glyph, so the hit area was a wide rectangle and the
   × sat off centre inside it. A fixed square with flex centring puts the glyph in the
   middle of the target rather than in the middle of whatever the padding left over.
   The border is present but transparent when idle, so hovering draws the box without
   changing the button's size and shifting the glyph under the pointer. */
.ph-viewer-close {
  position: absolute; top: clamp(.5rem, 2vw, 1rem); right: clamp(.5rem, 2vw, 1rem); z-index: 2;
  width: 44px; height: 44px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 3px solid transparent; cursor: pointer;
  color: var(--ink);
  transition: border-color .2s var(--ease);
}
.ph-viewer-close svg {
  display: block; width: 18px; height: 18px;
  stroke: currentColor; stroke-width: 2.5; fill: none; stroke-linecap: butt;
}
.ph-viewer-close:hover, .ph-viewer-close:focus-visible { border-color: var(--ink); }

/* Paging arrows. Same square, same 3px box on hover, same drawn stroke as the close, so
   the three controls in this view are one family rather than three different objects. */
.ph-viewer-nav {
  position: absolute; top: 50%; transform: translateY(-50%); z-index: 2;
  width: 44px; height: 44px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 3px solid transparent; cursor: pointer;
  color: var(--ink);
  transition: border-color .2s var(--ease);
}
.ph-viewer-nav:hover, .ph-viewer-nav:focus-visible { border-color: var(--ink); }
.ph-viewer-prev { left: clamp(.5rem, 2vw, 1rem); }
.ph-viewer-next { right: clamp(.5rem, 2vw, 1rem); }
.ph-viewer-nav svg {
  display: block; width: 18px; height: 18px;
  stroke: currentColor; stroke-width: 2.5; fill: none; stroke-linecap: butt; stroke-linejoin: miter;
}
/* The picture fades between pages rather than cutting. .swapping is held for exactly the
   length of this transition by the script, which swaps the src at the far end of it, so
   the outgoing and incoming pictures never overlap. */
.ph-viewer-stage img { transition: opacity .2s var(--ease); }
.ph-viewer.swapping .ph-viewer-stage img { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .ph-viewer-stage img { transition: none; }
}
body.ph-viewer-open { overflow: hidden; }
@media (prefers-reduced-motion: reduce) { .ph-viewer { transition: none; } }

/* ═══════════════════ LEGAL ═══════════════════ */
/* CENTRED, LIKE THE NEWS PAGES (Cam, 10 Sep: "I need the header and subheaders to all be
   aligned with the body", then "it feels quite close to the left, it should be positioned the
   same as the news pages, so centre").
   The measure was already centred; what was wrong is that .page-head above it was not, so the
   title and standfirst started 214px left of every word beneath them. First pass moved the
   BODY left to meet the head. This moves the HEAD to meet the body instead, which is the
   arrangement a news article already uses: a centred column of running text under a centred
   head, and it is the one Cam wants here.
   The block is centred, the text inside it is not. Same as .article. */
/* THE SAME MEASURE AS AN ARTICLE (Cam, 10 Sep: "is the cookies and hire agreement page taking
   on the same text styling as the pages on news, the hierarchy / leading etc. should be the
   same"). It was not: 800px against .article's 680, which is a different measure for the same
   job, running text a reader works through top to bottom. */
.legal { max-width: 680px; margin-inline: auto; }
/* CONTENTS IS A LABEL, NOT A BOLD (Cam, 10 Sep: "some kind of bold that we don't have
   anywhere else on the site"). It was `style="font-weight:600"` inline in both pages, and
   600 appears NOWHERE in this stylesheet: every heading on the site is 400 and separates
   itself by size, colour and space instead. So does this now, at the same size as the list
   below it, in --mid-gray with the small-text tracking the site gives 13-15px type, over the
   2px rule that .cat-group h3 already uses to head a list. */
.toc-title {
  font-size: var(--fs-15); font-weight: 400; color: var(--mid-grey);
  letter-spacing: var(--track-small);
  margin-bottom: .55rem; padding-bottom: .35rem;
  border-bottom: 2px solid var(--line);
}

/* --fs-lead, not --fs-25: .article-body h2 is 22px and this was 24px, so a clause heading
   outweighed a section heading in a news piece for no reason. Margins take .article-body's
   too, which is what actually sets the rhythm down a long page. */
.legal h2 { font-size: var(--fs-lead); line-height: var(--lh-body); margin-block: 2.2rem .8rem; }
.legal h2:first-child { margin-top: 0; }
/* h3 EXISTS ON THIS PAGE NOW, for the call-out policy's subheads, and had no rule at all, so
   it fell to the browser default: bold, and larger than the body it heads. Bold is the thing
   Cam pulled out of the Contents label, and it is not used anywhere on this site.
   --fs-body against h2's --fs-25 is a clear step down, and 400 keeps it in the same family as
   every other heading here. The separation is the space above it, not the weight. */
.legal h3 { font-size: var(--fs-body); font-weight: 400; line-height: var(--lh-body); margin-block: 1.8rem .5rem; }
/* .article-body's own values, --fs-body on --lh-body with 1.5rem beneath. These were a
   hardcoded 1.0625rem (17px) on 1.7 with 1rem beneath, which is not a step in the type scale
   at all and was both smaller and more loosely led than the news pages it now sits beside. */
.legal p, .legal li { font-size: var(--fs-body); line-height: var(--lh-body); margin-bottom: 1.5rem; }
/* ─── Fine print ──────────────────────────────────────────────────────────────
   A step down for the enumerated sub-clauses: the (a)(b)(c) runs that qualify a clause rather
   than state it. The source document sets these smaller too, so this is the agreement's own
   hierarchy rather than one invented for the web, and it is what lets a reader see the shape
   of the agreement, sixteen numbered obligations, without the qualifications competing.
   --fs-15 against --fs-body, on the same --lh-body so the two sit in one rhythm. Kept at the
   same colour: this is smaller, not quieter, and greying out terms someone is agreeing to
   would be the wrong signal entirely. */
.legal .fine { font-size: var(--fs-15); line-height: var(--lh-body); margin-bottom: 1.5rem; }
.legal ul { padding-left: 1.2rem; }
.legal ul li { list-style: disc; margin-bottom: .5rem; }
.legal .toc { background: var(--paper); border: 2px solid var(--line); padding: 1.6rem 2rem; margin-bottom: 3rem; }
.legal .toc a { color: var(--ink); }
.legal .toc li { margin-bottom: .4rem; }
/* THE CONTENTS LIST IS NOT PROSE, so it drops the disc and the indent `.legal ul` sets for
   the lists inside the clauses. This was two inline styles on the <ul>, the same shortcut
   that put the weight on the heading. It has to sit AFTER `.legal ul li`, which is the same
   (0,2,0) and would otherwise win on order and put the bullets back. */
.legal .toc ul { list-style: none; padding-left: 0; }
.legal .toc ul li { list-style: none; }

/* ═══════════════════ BLACK CONTACT SYSTEM ═══════════════════ */
body.contact { background: var(--ink); color: #fff; }
body.contact header.site { background: transparent; }
/* The bar's white strip is drawn by .header-inner::before, which stayed white on this
   dark page, so the white burger and white nav type were invisible against it. */
body.contact .header-inner::before { background: var(--ink); border-bottom-color: #fff; }
body.contact .nav-list a, body.contact .nav-contact a { color: #fff; }
body.contact .nav-quote-btn { background: var(--yellow); color: var(--ink) !important; }
body.contact .nav-burger span { background: #fff; }
body.contact .page-head h1 { color: #fff; }
body.contact .page-head .sub, body.contact .breadcrumbs, body.contact .breadcrumbs a { color: rgba(255,255,255,.6); }
body.contact .breadcrumbs a:hover { color: #fff; }
body.contact .u { color: #fff; }
/* black form block sits seamlessly on the black page */
/* body.contact was request-a-quote.html, removed 10 Sep. Nothing carries the class now. */

/* fade-to-black page transition */
.page-wipe { position: fixed; inset: 0; background: var(--ink); z-index: 99999; pointer-events: none; transform: translateY(100%); transition: transform .55s var(--ease); }
.page-wipe.cover { transform: translateY(0); }
.page-wipe.revealed { transform: translateY(-100%); }
.page-wipe.no-anim { transition: none; }

/* load-in splash — yellow with large Kitto logo */
.loader { position: fixed; inset: 0; background: var(--yellow); z-index: 100000; display: flex; align-items: center; justify-content: center; transition: opacity .45s var(--ease), visibility .45s; }
.loader img { width: clamp(210px, 36vw, 510px); height: auto; }
.loader.hide { opacity: 0; visibility: hidden; }

/* ═══════════════════ FOOTER ═══════════════════ */
/* No rule between the links and the small print. With the links on one line the two are
   plainly separate already, and the border was fencing off a single sentence. Closer in,
   too: the space it needed to clear is space the line no longer needs. */
/* The footer is now the mark and one line of small print. The row of links that used to sit
   between them is gone: it repeated the bar at the top of the page, which is fixed and never
   out of reach, so it was restating the navigation rather than closing the page. */

/* home equipment section sits on a white band, incl. the 'Browse all equipment' button */
/* TIGHTER THAN A DEFAULT SECTION (Cam, 19 Aug). This block is a heading, three tiles and
   nothing else now that the "Browse all equipment" button has gone, so the standard --sec
   band of up to 110px top and bottom left it floating in white. About two thirds of that,
   and the gap under the heading comes in with it. */
.selections-block { background: var(--white); padding-block: clamp(40px, 5.5vw, 72px); }
.selections-block .section-head { margin-bottom: clamp(1.1rem, 2vw, 1.7rem); }

/* kit-at-work slider is now the last section, below the footer */
.slider-block { padding-top: 0; }
/* standardised yellow divider between the footer and the Kit-at-Work slider — same on every page */
/* The yellow band carries the copyright now, so it is a band of type rather than a band of
   colour. Black on the yellow, not the grey the line had inside the footer: on this ground
   grey goes muddy, and the small print is the last thing on the page, so it may as well be
   legible. min-height rather than height, so a long line on a narrow screen wraps inside the
   band instead of spilling out of it. */
/* Was one centred line. Now the credit sits left and the legal links right, on the same
   rule, so the agreement and the cookies policy have a home on every page. */
.footer-spacer {
  min-height: clamp(2.5rem, 5vw, 4rem); background: var(--yellow);
  /* LEFT ALIGNED, EVEN GAPS (Cam, 24 Aug). space-between was right when the band held two
     things and they wanted opposite edges; with four groups in it that left a hole in the
     middle and jammed them into the corners. Distributing the space instead, evenly or
     otherwise, sets the gaps from whatever the band's width happens to be, so the row
     re-spaces itself at every screen size and lines up with nothing.
     flex-start puts every item on the same left gutter the rest of the page starts from,
     and the gap below is then the ONE number that sets the rhythm, identical between all
     of them and identical at every width. */
  display: flex; align-items: center; justify-content: flex-start;
  gap: .4rem 1.5rem; flex-wrap: wrap;
  padding: .9rem var(--gut);
  font-size: var(--fs-12); color: var(--ink);
}
/* THE GROUPS DISSOLVE (Cam, 24 Aug). They exist in the markup to say what each link is,
   which is worth keeping for a screen reader, but as boxes they were the reason the
   spacing could not be even: the band could only distribute space BETWEEN the four
   groups, while inside each one the links kept their own 1.25rem. display: contents drops
   the boxes and hands every link to the band directly, so all of them are laid out by the
   one space-evenly and every gap in the row is the same gap. */
/* .footer-contact went with the phone number and the email; only .footer-legal is built
   now. display: contents so its two links are laid out by the band directly and every gap
   in the row is the band's one gap, rather than the band spacing a group and the group
   spacing its own links. */
.footer-legal { display: contents; }
/* No centring on a phone either. The band used to centre once it wrapped, because with a
   credit on one edge and the links on the other there was no left and right left to hold.
   Everything starts on the left now, so a wrapped line starts there too and the footer
   keeps one edge at every width. */

/* ═══════════════════════════════════════════════════════════════
   RESPONSIVE
═══════════════════════════════════════════════════════════════ */
@media (max-width: 1200px) {
  :root { --gut: 56px; }
  .partnership-inner { gap: 3rem; }
}
@media (max-width: 992px) {
  :root { --gut: 24px; }
  .nav-list { display: none; }          /* hide the text links on mobile… */
  .nav-basket { display: inline-flex; } /* …but keep the basket reachable */
  .nav-right { margin-left: auto; }     /* group basket + burger together on the right */
  .header-inner { gap: 1.1rem; }
  .nav-burger { display: block; }
  .intro-inner { grid-template-columns: 1fr; gap: 1.5rem; }
  .selections-item { grid-template-columns: 1fr 1fr; }
  .selections-item .line { display: none; }
  .selections-item-text { grid-column: 1 / -1; }
  .kit-grid { grid-template-columns: repeat(2, 1fr); }
  .newsletter-grid { grid-template-columns: 1fr 1fr; }
  .news-feature { grid-template-columns: 1fr; }
  .slider-item { flex-direction: column; }
  .slider-item-left { width: 100%; padding-right: 0; margin-bottom: 1.5rem; }
  /* .partnership-inner holds its two columns past this width now, see the 880 block below:
     at 913 there is still room for the link and the logos beside the text. */
  /* .form-inner-wrap holds its two columns past this width now, see the block at the end of
     the stylesheet: at 650 there is still room for the intro beside the form. */
  .form-inner { column-gap: 28px; }
  .kit-list-wrapper { grid-template-columns: 1fr 1fr 1fr; gap: 3rem; }
  .about-intro { grid-template-columns: 1fr; }
  .team-grid { grid-template-columns: repeat(2,1fr); }
  .value-grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
  .button { font-size: var(--fs-body); padding: 12px 22px; }
  /* HERO ON A PHONE: the same five pictures, but one at a time in a swipeable strip.
     The two-column mosaic cannot survive at this width, the tiles come out as postage
     stamps, so the arrangement changes rather than the content. Grey field, yellow
     stroke and the same yellow credit block, just one across with tighter margins.
     No JavaScript: the strip is a scroll-snap container, so it swipes with the phone's
     own scrolling, keeps its momentum and needs no arrows. The next picture peeks in at
     the edge, which is what tells you there is more to swipe to. */
  body.home .hero-band {
    --hero-inset: 16px;
    --hero-gap: 12px;
    /* More clearance above than below: the bar sits directly on top of the strip, and an
       even margin left the first picture feeling tucked under it. */
    /* SUPERSEDED 24 Aug: there IS a bar to clear again. The statement moved below the
       pictures, so the strip is the first thing on the page and the 40px here left the top
       of every photograph under the header. The bar's measured height plus the band's own
       inset, which is the same sum the desktop rule uses. The bottom stays deeper than the
       top so the grey carries past the strip rather than ending on it. */
    padding: calc(max(72px, var(--header-h, 72px)) + var(--hero-inset)) 0 52px var(--hero-inset);
  }
  /* Nothing for the lead picture here on purpose: its desktop sizing is behind a
     min-width query, so on a phone it inherits the strip's tile rules like any other slide. */
  .hero-mosaic {
    display: flex; height: auto;
    /* HORIZONTAL ONLY (Cam, 19 Aug). Setting overflow-x to auto makes the OTHER axis
       compute from visible to auto, which is what let the strip be dragged up and down as
       well as sideways. overflow-y has to be stated to stop it. */
    overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;                 /* the pictures are the affordance, not a bar */
  }
  .hero-mosaic::-webkit-scrollbar { display: none; }
  /* display: contents dissolves the two column wrappers, so all five tiles become direct
     children of the strip and can be laid out as one row. */
  .hero-col { display: contents; }
  .hero-tile {
    flex: 0 0 86%;                         /* the missing 14% is the peek */
    /* WAS 1/1 (Cam, 24 Aug). Square was already a step up from 4:3, but a phone has more
       height to spare than that used, and at the top of the page the pictures are what
       the reader has come for. 1/1.3 is the square plus 30%: same width, so the peek and
       the snap are untouched, and only the depth changes. A taller frame crops a 3:2
       photograph harder across its width, which is what the object-position lines below
       are already there to steer. */
    aspect-ratio: 1 / 1.3;
    margin: 0 var(--hero-gap) 0 0;         /* clears the desktop stagger insets */
    scroll-snap-align: center;
  }
  /* REMOVED 10 Sep. This was `.hero-tile:last-child { margin-right: var(--hero-inset) }`,
     holding the final tile of the horizontal swipe strip off the screen edge. Everything in
     this block is superseded by the stacked, centred layout in the later max-width: 768px
     block, and this one declaration was the only part still reaching through it: there is no
     strip edge left to clear, and any margin-right here beats that block's `margin-right:
     auto` on specificity and pushes whichever tile it lands on off centre.
     Re-scoping it did not help, it only moved the offset from Prinks to Sophie Clements,
     because the problem is the declaration existing at all, not which tile it picks. */
  /* A square crop of a 3:2 photograph keeps only the middle two thirds of its width, and
     in this one the CRT stack stands at the right-hand edge, so a centred crop cut the
     screens off. Holding the crop right shifts the picture left and the stack is back in.
     Mobile only: at desktop the tile is wide enough that the whole room fits. */
  .hero-tile.tile-e img { object-position: 92% center; }
  /* THE CREDIT BECOMES A BAND ALONG THE BOTTOM (Cam, 24 Aug). It was a shrink-to-fit
     yellow block in the top right, sized off the longest line, so on a square tile it cut
     a ragged step out of the corner of the picture and every tile's block was a different
     width. Full width along the bottom edge instead: one shape on every tile, the top of
     the picture is clear, and the type starts at the left where the eye does.
     max-width has to go with it, or the block keeps sizing to its content and never
     reaches both edges. text-align follows for the same reason it was right before:
     the type should sit against the edge the block is anchored to. */
  .hero-tile-cap {
    top: auto; bottom: 0; left: 0; right: 0;
    max-width: none;
    text-align: left;
    padding: 8px 12px;
  }
  /* THE CAPTION FLOWS AS ONE LINE OF TEXT (Cam, 24 Aug). It is a flex COLUMN, so each span
     is a line whatever its length: the work title got a line to itself, and a short one
     like "Prinks" left most of a full-width band empty above two long lines. Set inline
     they run on as a sentence and wrap only where they run out of room, which is both
     better ragged and shorter, and the band takes two lines instead of three.
     Comma before each following span, exactly as .slider-cap does it in the footer
     carousel, so the two captions on the page are punctuated the same way.
     text-wrap normal, not pretty: pretty shortens lines to avoid an orphan, and the whole
     point here is to fill the measure. */
  .hero-tile-cap { display: block; }
  .hero-tile-cap span { display: inline; text-wrap: normal; }
  .hero-tile-cap .cap-meta::before { content: ", "; }
  /* NO EXPAND ON A PHONE (Cam, 24 Aug). With no hover to bring it in, the button sat
     permanently over the bottom left of every tile in the strip, so a control that is a
     quiet offer on desktop became a fixture on the picture. The tile still links through
     to its category, which is the route that matters here; only the way to the uncropped
     photograph goes, and the expanded stage simply never opens at this width.
     display: none rather than opacity, so it leaves the tab order with it. */
  .hero-tile .hero-expand { display: none; }
  /* The strip's tiles are a square crop of a 3:2 frame, so the expanded view has the most
     to give here. Deeper than the band it replaces rather than matched to it: on a phone
     the picture is worth the scroll, and the hero is the top of the page, so what moves
     underneath moves down and out of the way rather than out from under the reader. */
  .hero-stage { height: min(70svh, calc(100svh - max(72px, var(--header-h, 72px)) - 2rem)); }
  /* EDGE TO EDGE on a phone. The tiles in the strip are a SQUARE CROP of a 3:2 photograph,
     so they are deep; the expanded view shows the whole frame, which on a portrait screen
     can only be as tall as its width allows. Held inside the band's inset, a landscape
     picture came out shorter AND narrower than the square it was expanded from, which read
     as the wrong way round however much more of the photograph it was showing.
     Width is the only thing there is to give on a phone, so the stage takes all of it: the
     negative margin cancels the inset the band holds on the left, and the band has none on
     the right, so the picture runs the full width of the screen and is plainly wider than
     the tile even where it is not as deep. */
  /* REVISED: the stage sits on an even margin instead of running edge to edge. Going full
     bleed was deliberate, width being the only thing a phone has to give, but it left the
     expanded picture the one element on the page with no margin at all. The band holds an
     inset on the left and none on the right, so the right is added back here and the two
     sides match. */
  .hero-stage { margin-inline: 0 var(--hero-inset); }
  /* The 1rem padding-inline that was here has gone: it contradicted the caption bar's own
     7px/12px and would have widened it on a phone. Dead either way — the expand button is
     hidden below 768, so the stage never opens at this width — but a rule that both cannot
     fire AND disagrees with the one it would override is worth not leaving behind. */
  /* The band handles clearance for the fixed bar itself, as it does on desktop. Leaving
     the body padding on as well put a strip of bare page above the grey. */
  body.home { padding-top: 0; background: var(--white); }
  .selections-item { grid-template-columns: 1fr; }
  /* Two columns still hold here. The drop to one happens at 576, not at this breakpoint:
     by 593 the pair is comfortable and one column would waste the width. */
  .kit-grid { grid-template-columns: 1fr 1fr; gap: 1.6rem 1rem; }
  /* JOURNAL AS A VERTICAL STACK (Cam, 24 Aug). It was a swipe strip built to the same
     numbers as the picture strip below the equipment block. The strip only ever showed one
     card at a time and hid the rest behind a gesture nothing on the page announced, so the
     posts now simply stack full width and are read by scrolling the page, like everything
     else on a phone. Nothing horizontal is left here: no flex, no snap, no overflow, and
     so no second axis to get stuck in. */
  .newsletter-grid { display: block; max-width: none; }
  .journal-head-card { max-width: none; margin-bottom: 2rem; }
  .newsletter-strip { display: block; max-width: none; }
  /* 3rem was set when each card still carried its standfirst and needed the separation to
     stop the paragraphs running into the next picture. With the standfirst gone on the
     home page a card is a picture, a date and a title, so the cards can sit closer and
     read as one list rather than four blocks. */
  .newsletter-grid .news-card { margin-right: 0; margin-bottom: 1.5rem; }
  .newsletter-grid .news-card:last-child { margin-right: 0; margin-bottom: 0; }
  .newsletter-grid .news-card:nth-child(2n) { margin-top: 0; }   /* no terrace offset in a stack */
  /* One proportion down the stack. The terrace deliberately alternates 4/3 and 3/2, and one
     card is 4/5, which is what makes the grid worth looking at; in a single full-width
     column it just leaves cards of three different depths and no way to see the pattern. */
  .newsletter-grid .news-card .ph,
  .newsletter-grid .news-card .news-card-media,
  .newsletter-grid .news-card.news-card--tall .ph,
  .newsletter-grid .news-card.news-card--tall .news-card-media { aspect-ratio: 4/3; }
  /* NO STAGGER IN A STACK. The nth-child delays run to 1.5s, which is right when several
     cards enter together in the desktop terrace. Stacked, each card is observed on its own
     and enters alone, so the delay is just the card sitting there blank after you have
     already scrolled to it. The :nth-child(n) is there to match the specificity of the
     delays it is cancelling, which are themselves nth-child rules. */
  .newsletter-grid .news-card:nth-child(n) { transition-delay: 0s; }
  /* THE STANDFIRSTS ARE BACK ON A PHONE (Cam, 26 Aug: "we need to get news descriptions
     back into the home page"). This hid them on 24 Aug, home page only, on the reasoning
     that four stacked pictures each with a paragraph under it made the block read as the
     end of the page. Reversed: the descriptions are what the cards are for, and a title
     alone does not say enough to earn the tap.
     The rule is left here, commented, rather than deleted, because it is the one place
     that records the block ever behaved differently on a phone.
     body.home .newsletter-grid .news-card > p:not(.title) { display: none; } */
  /* no padding on the overflow-clipped viewport — it broke the translateX(-n*100%) math and made slides peek/misalign */
  .slider-viewport { padding-inline: 0; }
  /* The footer carousel was 3/2, about 260px on a phone, which made these installation
     shots read as thumbnails. Height off the viewport instead, so a slide is roughly
     twice what it was and fills most of the screen the way it does on desktop. */
  /* THE SLIDE IS THE SCREEN (Cam, 24 Aug). 72vh left a band of the page showing under
     every picture, so a slide read as a tall picture on a page rather than as the picture
     you are looking at. Full viewport height instead.
     svh, not vh: vh on a phone is the height with the browser chrome RETRACTED, so a slide
     set to 100vh is taller than the window whenever the address bar is showing and the
     bottom of every photograph is cut off until you scroll. svh is the stable smaller
     value, so the picture fits the window in both states. */
  .slider-slide img { aspect-ratio: auto; height: 100svh; }
  /* The phone-only --fs-13 that used to be here has gone: the caption is --fs-13 at every
     width now, set on .slider-cap itself, so an override saying the same thing on a phone
     was one more place for the two to drift apart. */

  /* WHERE EACH PHOTOGRAPH IS CROPPED FROM, ON A PHONE ONLY (Cam, 19 Aug).
     A tall frame crops a 3:2 photograph HORIZONTALLY, the opposite of desktop, where a
     16:9 frame crops it vertically. So these are mobile-only and say nothing about the
     desktop crop, which is why the clements rule further up still reads `center 75%`.
     The numbers are not guesses. A 3:2 source at 72vh on a 390px screen is drawn about
     900px wide in a 390px window, so 510px of it is off-screen and the subject has to be
     positioned deliberately or it is simply missed. Each value is the position that centres
     that photograph's screen or projection in the window, clamped where the subject sits so
     far to one side that the edge is as far as it goes.
     Read off the photographs one by one rather than applied as a blanket rule: in every one
     of these the thing being hired is a screen somewhere off centre, and a centred crop cut
     it out. */
  .slider-slide img[src*="work-clements-1"] { object-position: 100% center; }   /* CRT stack, hard right */
  .slider-slide img[src*="work-faramawy-1"] { object-position: 0% center; }     /* projection wall, left */
  .slider-slide img[src*="work-faramawy-2"] { object-position: 57% center; }    /* the two monitors sit at ~54% of the frame; 68% left them hard right of centre */
  .slider-slide img[src*="work-lapelyte-1"] { object-position: 0% center; }     /* screen, far left */
  .slider-slide img[src*="work-radage-1"]   { object-position: 0% center; }     /* monitor, far left */
  .slider-slide img[src*="work-radage-2"]   { object-position: 100% center; }   /* monitor, far right */
  .slider-slide img[src*="work-radage-3"]   { object-position: 45% center; }    /* projection is wider than the window, favour its left edge */
  .slider-slide img[src*="work-radage-4"]   { object-position: 0% center; }     /* monitor in soil, far left */

  .slider-cap { bottom: 1.2rem; }
  .slider-credit { right: calc(var(--gut) + 1rem); bottom: 1rem; }
  .form-inner { grid-template-columns: 1fr; }
  .kit-list-wrapper { grid-template-columns: 1fr 1fr; }
  .team-grid { grid-template-columns: 1fr 1fr; }
  /* Three across, not two. Eight logos two-up made a tall narrow ladder that pushed the
     rest of the block down the screen, and at half the width each mark was sitting bigger
     than the text it belongs under. Three fits the row to the phone and reads as a set of
     client marks rather than a list. Eight over three leaves two on the last row, which is
     what a client list does; they stay left, in step with the columns above. */
  /* The partnership logos are handled in their own two blocks at the end of the stylesheet,
     880 and 576, because the widths they change at are not the site's usual ones. */
  .cta-band .container { flex-direction: column; align-items: flex-start; }

  /* ARTICLE PICTURES 30% TALLER (Cam, 24 Aug). 16/9 across a 302px screen is a 170px
     letterbox, which is a thumbnail of a photograph rather than a look at one. 16/11.7 is
     the same frame plus 30%: the width is fixed by the column either way, so depth is the
     only thing there is to give, and these are the pictures the article is written around.
     Specificity is deliberate. .ph-tall and the .ph-pair cards are already upright at 4/5
     and are left alone: both are two-class selectors and outrank this one. */
  .article-body .ph { aspect-ratio: 16 / 11.7; }
  .media-full .ph { aspect-ratio: 16 / 11.7; }
  /* NO EXPAND HERE EITHER, same reason as the hero tiles: with no hover to bring it in the
     button sits permanently on the picture, and a taller picture is already showing more of
     the frame than the letterbox it replaced. */
  .article-body .ph .hero-expand, .media-full .ph .hero-expand { display: none; }
}
@media (max-width: 576px) {
  :root { --gut: 16px; }
  .button { width: 100%; }
  .hero-actions { flex-direction: column; align-items: stretch; }
  /* SUPERSEDED 24 Aug: the grid is one column now, see .kit-grid in the 768 block. The
     principle that note arrived at still holds and is what the new rule is built on:
     narrowing the TILE is the answer to the billboard, not capping its height, because a
     capped tile is a short wide box with a contained photograph marooned in the middle.
     The card now carries a max-width, so the tile is narrow without being one of two. */
  /* ONE COLUMN, BUT ONLY HERE (Cam, 24 Aug). Two-up leaves each card about 150px wide at
     333, which is not enough for the name, the tags and the price to sit without breaking.
     Above this breakpoint the pair is comfortable and stays.
     The billboard problem the note above describes is real, so the card is narrowed and
     centred rather than the tile being capped: the photograph keeps its shape and fills
     whatever frame it is given, the frame is just no longer the width of the phone. Same
     treatment as .home-cat, so the two category grids agree.
     Row gap comes down from 1.6rem with the column count: one column of tiles this size
     reads as a list, and a list wants less air between items than a grid does. */
  .kit-grid { grid-template-columns: 1fr; gap: 1.5rem; }
  .kit-card { max-width: min(320px, 74vw); margin-inline: auto; width: 100%; }
  /* NAMES PULLED IN TO THE PICTURE (Cam, 26 Aug: "too far away from the image itself").
     The padding was never the problem, and cutting it alone would have moved this about
     five pixels. THE GAP WAS THE SQUARE TILE. `object-fit: contain` draws the photograph
     to its own proportion inside the tile, so a landscape shot in a 1/1 box fills the
     width and letterboxes top and bottom: on a 289px tile a 3:2 photograph left roughly
     48px of empty tile under the picture before the name's own padding even began.
     4/3 is the shape the product photography actually is, so the dead band drops to about
     12px and a 4:3 shot fills the tile outright. The picture is not smaller, it is the
     same width it always was, with the empty half-inch under it taken away.
     The name's padding comes down with it, now that it is measuring from the ink. */
  .kit-card-body { padding-top: .45rem; }
  .home-cat-body { padding-top: .45rem; }
  .kit-card-media { aspect-ratio: 4/3; }
  /* The credit is BACK on a phone. It was hidden here when it read "© Kitto, all rights
     reserved, designed by Cameron Harris", which wrapped to three lines at 320px and became
     the largest thing in the band. The designer credit has since gone and what is left is
     short enough to sit on one line beside the two policy links. */
  .kit-card-media img { width: 100%; height: 100%; }
  .kit-list-wrapper { grid-template-columns: 1fr; }
  .team-grid { grid-template-columns: 1fr; }
  .info-stats { gap: 2rem; }
}

/* ─── Checkout, a view inside the basket panel ──────────────────
   It used to be a fixed overlay of its own, rising from the bottom of the screen with its own
   scrim at z-index 60, while the basket dropped from under the bar. Two panels meant two open
   states and the bar's control only governed one of them. It is a view in the basket's panel
   now: no fixed positioning, no scrim, no entrance of its own, because the panel it lives in
   already has all three. The panel provides the ground, the border and the scrolling. */
.checkout[hidden] { display: none; }
.checkout { position: relative; }
/* Basket and checkout are two views inside one panel, swapped by toggling display, so the
   change happened in a single frame and read as a repaint rather than a move between views.
   Each view now fades and rises as it takes over. Un-hiding a display:none element restarts
   its animation, which is what makes this replay on every swap without any JS. */
.basket-view, .checkout { animation: coViewIn .18s var(--ease) both; }
@keyframes coViewIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .basket-view, .checkout { animation: none; } }
.checkout-panel {
  position: relative; width: 100%; max-width: 760px; margin-inline: auto;
  background: transparent; color: inherit;
  padding: clamp(1.6rem, 4vw, 2.6rem);
  animation: coIn .3s var(--ease);
}
@keyframes coIn { from { opacity: 0; } to { opacity: 1; } }
/* Back to the basket, not out of the panel. The way out is the cross in the bar, which is the
   point of the whole rearrangement. */
/* Was a 1.9rem × glyph. It is a named action now — "View basket" — because the control goes
   back to the basket rather than closing the panel, and a × said the opposite. Sized to
   .co-mode-toggle so the two text controls in this panel match. */
/* Static and centred rather than absolutely positioned, so its own bottom margin IS the
   break between it and the dates row underneath (Cam, 19 Aug). */
.checkout-close {
  position: static; transform: none;
  display: block; width: fit-content;
  margin: 0 auto clamp(1.75rem, 3.5vw, 2.5rem);
  /* TEXT, NOT A BUTTON (Cam, 10 Sep: "back to basket to be the same size as Add a start date
     and hire length, and is underlined italic on hover").
     It has been every size a box can be today, 15px, then .button's 12/24, then a step above
     it. The panel has since become two ruled grids, and in that setting a lone outlined box
     floating above them was the only thing on screen pretending to be a control rather than
     being one. Set as the prompt below it instead, 15px underlined, italic on hover: the same
     treatment "set exact dates" takes, which is the other way out of this view.
     Padding and border go entirely; the panel's own spacing is what holds it clear. */
  padding: 0; border: 0;
  font-size: var(--fs-15); line-height: var(--lh-narrow);
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
  font-style: normal;
  background: transparent;
  transition: background-color .2s var(--ease), color .2s var(--ease);
}
/* Fades while the length/dates controls are swapped underneath. See SWAP_MS in main.js. */
.checkout-dates { transition: opacity .13s var(--ease); }
.checkout-dates.is-swapping { opacity: 0; }
.checkout-close:hover, .checkout-close:focus-visible { background: transparent; font-style: italic; }
/* checkout head — one uniform text size across the whole duration block */
.checkout-head { margin-bottom: 1.5rem; font-size: var(--fs-15); }
.checkout-dates { display: flex; flex-direction: column; gap: .85rem; }
/* CENTRED AS A GROUP (Cam, 10 Sep: "start date and hire length become centred so those become
   relational, but they are still left aligned by container, just centred in the checkout").
   justify-content centres the ROW, so the pair sits under the prompt on the panel's axis; each
   field inside keeps align-items: flex-start, so its label still sits on the field's own left
   edge rather than over the middle of it. The two are different axes and this is the one place
   on the panel where they disagree on purpose. */
.checkout-dates-row { display: flex; align-items: flex-start; justify-content: center; gap: 1.4rem; flex-wrap: wrap; }
.checkout-dates [hidden] { display: none !important; }   /* label/flex display would otherwise override the hidden attribute */
.checkout-dates label { font-size: var(--fs-15); color: var(--mid-grey); display: inline-flex; flex-direction: column; align-items: flex-start; gap: .35rem; }
/* BOXES, NOT UNDERLINES (Cam, 10 Sep: "start date and days should be in boxes rather than
   not, consistent with the rest").
   The date, the number and the unit were the only controls on this panel drawn as an
   underline: the steppers beside them, .checkout-close above them and every .button on the
   panel are 2px boxes. Three fields reading as text with a rule under them, in a row with two
   boxed steppers, is what made the block hard to parse as a form.
   Same 2px, same zero radius, and the padding grown to hold the type off the stroke. */
/* One height for every control on this row. Declared on .checkout-dates so the date field and
   the length box both read it from the same place. */
.checkout-dates { --co-field-h: 2.45rem; }
.checkout-dates input[type="date"] { font: inherit; font-size: var(--fs-15); text-transform: none; letter-spacing: 0; color: var(--ink); background: transparent; border: 2px solid var(--ink); padding: 0 .5rem; border-radius: 0; min-width: 8rem; height: var(--co-field-h); }
/* checkout panel is white — show the calendar picker as a dark icon (the global rule inverts it to white for dark sections) */
.checkout-dates input[type="date"]::-webkit-calendar-picker-indicator { filter: none; opacity: .9; cursor: pointer; }
.checkout-dates input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
.checkout-duration { font-size: var(--fs-15); color: var(--ink); border-bottom: 2px solid transparent; }
/* hire-length stepper: start date + [– N +] [unit], underlined to match the date field */
.checkout-dates .co-date-label { font-size: var(--fs-15); color: var(--mid-grey); }
.co-length { display: inline-flex; flex-direction: column; align-items: flex-start; gap: .35rem; }
/* ONE JOINED BOX (Cam, 10 Sep). Was a .4rem gap between four separate controls; the box now
   lives HERE, on the wrapper, and the two fields inside it drop their own borders so the
   number and the unit read as one answer with a rule between them rather than as two
   questions. align-items: stretch so the divider runs the full height of the box. */
.co-length-controls {
  display: inline-flex; align-items: stretch; gap: 0;
  border: 2px solid var(--ink); border-radius: 0;
  overflow: hidden;
  /* THE TWO FIELDS FILL THE BOX (Cam, 10 Sep: "the word days is still off").
     They were computing to 46px inside a 39px box and overflowing it, so `overflow: hidden`
     clipped them and the text sat high in what was left. align-items: stretch does not help
     when a child's own intrinsic height already exceeds the container: it has to be told.
     height: 100% pins both to the box, and the line-height is the box's INNER height, the
     token less its two borders, which is what actually centres the type. The select is why
     this matters: an input centres its own value, a select does not, and line-height is the
     only thing that moves it. */
  /* SAME HEIGHT AS THE DATE FIELD (Cam, 10 Sep: "please make sure the height of the box start
     date and hire length is the same height"). They are two boxes on one line answering one
     question, and they were within a couple of pixels of each other by accident, since a date
     input and a flex row of a number and a select each work their height out from different
     defaults. Both take --co-field-h now, so they cannot drift apart when either is retuned. */
  height: var(--co-field-h);
}
.co-length-controls > * {
  height: 100%; min-height: 0;
  line-height: calc(var(--co-field-h) - 4px);
}
.co-step { position: relative; width: 1.55rem; height: 1.55rem; flex: none; border: 2px solid var(--ink); border-radius: 0; font-size: 0; line-height: var(--lh-flat); padding: 0; color: var(--ink); background: transparent; cursor: pointer; transition: background .2s var(--ease), color .2s var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .co-step:hover { background: var(--ink); color: #fff; }
}
/* Inside the joined box now, so no border of its own; the rule on its right IS the divider.
   Wider than it was: with no steppers beside it the field is the only place a number is
   typed, and it has to look like one. */
#checkoutQty { font: inherit; font-size: var(--fs-15); width: 3.4rem; text-align: center; color: var(--ink); background: transparent; border: 0; border-right: 2px solid var(--ink); padding: 0 .4rem; border-radius: 0; -moz-appearance: textfield; appearance: textfield; }
#checkoutQty::-webkit-outer-spin-button, #checkoutQty::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
/* THE ARROW IS CENTRED NOW (Cam, 10 Sep: "drop down arrow is unaligned and not centred").
   Its two halves were pinned 14px from the TOP of the control, a figure that was right for the
   padding this select used to have and wrong the moment the box changed height. `center` on
   both halves puts the caret on the field's own middle and keeps the two halves registered to
   each other, so it cannot fall out of alignment again when the height is retuned. */
#checkoutUnit { font: inherit; font-size: var(--fs-15); text-align: left; text-align-last: left; color: var(--ink); background: transparent; border: 0; padding: 0 1.6rem 0 .7rem; border-radius: 0; cursor: pointer; appearance: none; background-image: linear-gradient(45deg,transparent 50%,var(--ink) 50%),linear-gradient(135deg,var(--ink) 50%,transparent 50%); background-position: calc(100% - 13px) center, calc(100% - 8px) center; background-size: 5px 5px,5px 5px; background-repeat: no-repeat; }
#checkoutUnit option { background: var(--white); color: var(--ink); }
/* ABOVE THE TWO FIELDS, LEFT ALIGNED (Cam, 10 Sep: "can we make add a start date / set exact
   dates all left aligned", then "set exact dates feels like it should be on top of start and
   hire").
   It was pushed to the far right of the row by margin-left: auto, which put the control that
   SWITCHES the row at the opposite end of it, a long way from the two fields it switches
   between and on a different axis from everything else in the block.
   order: -1 with a full-width basis lifts it onto its own line above them, where it reads as
   what it is: the choice of how to answer, made before answering. Left, on the same margin as
   the sentence above it and the fields below. */
/* CENTRED UNDER BACK TO BASKET (Cam, 10 Sep: "centre it under 'back to basket' for both
   desktop and mobile"). .checkout-close is a block on `margin: 0 auto` at the top of this
   panel, so the prompt centring on the same axis puts the three lines, the way out, the
   instruction and its alternative, on one column before the form itself starts left.
   Both widths, so the mobile block below no longer has an opinion about this. */
.checkout-prompt {
  display: flex; flex-direction: column; align-items: center;
  /* TIGHTER (Cam, 10 Sep: "the lines are too leaded apart"). These are two halves of one
     sentence broken over two lines, not two paragraphs, so they want the leading of a single
     block: no gap at all, and --lh-narrow rather than the panel's body leading. */
  gap: 0; line-height: var(--lh-narrow);
  text-align: center;
}
/* 15px, matching the duration line above it (Cam, 10 Sep: "'instead' looks bigger than the
   rest of the text"). The button carried its own 15px but the word beside it inherited the
   panel's size from .checkout-head, so the two halves of one phrase were set differently. */
.co-mode-wrap { display: inline; font-size: var(--fs-15); }
/* A BOX TOO (Cam, 10 Sep: "set exact dates needs a box around it"). It was a .u, an
   underlined text link, sitting at the end of a row of boxed controls that it switches
   between: the one thing that changes the shape of that row looked like the only thing that
   was not part of it. Same box as .checkout-close, one step smaller, since it switches the
   row rather than leaving the panel. No underline: a box and a rule under the words say the
   same thing twice, which is the note already on .checkout-close. */
/* SET AS TEXT, NOT AS A CONTROL (Cam, 10 Sep: "that is underlined and on hover turns
   italic"). Every box it has worn, the outlined tab at the end of the field row and then the
   one on a line of its own, made it read as a third field beside the two it switches between.
   Underneath the sentence, underlined, it reads as the alternative to what the sentence just
   asked for.
   ITALIC ON HOVER, not a colour or a fill. It is the move the work titles make everywhere on
   this site, and it is the one hover that costs nothing here: a fill would put a block of
   yellow in the middle of a sentence, and there is no colour left to go to on a panel that is
   already yellow on grey. */
.co-mode-toggle {
  font-size: var(--fs-15); line-height: inherit;
  border: 0; border-radius: 0; padding: 0;
  background: transparent; color: inherit;
  font-family: inherit; cursor: pointer;
  text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;
  font-style: normal;
}
.co-mode-toggle:hover, .co-mode-toggle:focus-visible { font-style: italic; }
.co-saving { color: var(--mid-grey); }
/* one shared column grid so qty / rate / price line up across every item row */
.checkout-items { display: grid; grid-template-columns: 1fr auto auto; column-gap: clamp(.7rem, 3vw, 1.8rem); }
.checkout-item { display: grid; grid-template-columns: subgrid; grid-column: 1 / -1; align-items: center; padding: .85rem 0; border-top: 2px solid var(--line); font-size: var(--fs-body); }
.checkout-item:first-child { border-top: 2px solid var(--ink); }
/* ON A PHONE THE CHECKOUT LINE STACKS AND CENTRES (Cam, 19 Aug). The five-column subgrid
   gives the name whatever is left after the stepper, the rate, the line total and the
   remove ×, which on a 375px screen is a few characters and broke product names over three
   or four lines. Wrapping instead: the name takes a full line of its own, the controls and
   figures sit centred underneath where they can be read.
   THE BASKET IS DELIBERATELY LEFT ALONE — that one reads fine as it is. */
@media (max-width: 576px) {
  .checkout-items { display: block; }
  /* NOWRAP, so the quantity never flows with the text (Cam, 10 Sep, on a basket where a long
     name pushed the stepper onto a second line and left the x stranded beside it: "the number
     / quantity shouldn't flow with text but always need to be on the right", and "in that
     case ... to adjustable (rest of the text name)", meaning truncate the name instead).
     With wrapping allowed the row was two lines whenever the name was long and one when it
     was not, so the list had no rhythm and the controls moved between items. One line each
     now: the stepper and the x hold their places and the name gives ground, which is what the
     ellipsis is for. */
  .checkout-item {
    display: flex; flex-wrap: nowrap; justify-content: flex-start; align-items: center;
    grid-column: auto; gap: .55rem .9rem; text-align: left;
  }
  /* TWO LINES, LEFT ALIGNED (Cam, 10 Sep: "I'm not sure on the x on top of the items, ideally
     we could fit everything in one line, is there a way we can do an ellipsis to shorten
     names that would cause the line to break", then "I think we could do two lines max" and
     "it would then be left aligned").
     Everything above this was written for a centred four-line stack: the × on its own row at
     the top, then the name, then the stepper, then the total. Four lines per item on the
     screen with the least room, and the × read first on every one of them.
     Two lines now. The name takes the first with the × at the far end of it, and the stepper
     takes the second with the line total at the far end of that. Left aligned, so the names
     start on one axis and can be read down the list rather than each finding its own centre.
     THE NAME CANNOT WRAP, which is what guarantees the two. It truncates instead, which is
     the ellipsis Cam asked for: an item like "Adjustable 4-Leg Speaker Stand, Top-Mountable"
     would otherwise take three lines on its own and the count would be back to five.
     min-width: 0 is what makes that work. A flex item will not shrink below its content's
     intrinsic width without it, so the name would refuse to give ground and push the × off
     the screen instead of truncating. */
  .checkout-item .co-name {
    flex: 1 1 auto; min-width: 0; text-align: left;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* The type sits inside the name, so it truncates with it rather than adding a line. */
  .checkout-item .co-type { display: inline; margin-top: 0; }
  .checkout-item .co-type::before { content: ", "; }
  /* ONLY THE NAME FLEXES (Cam, 10 Sep: "you have so much room on the right here to be used").
     The stepper was `flex: 1 1 auto` and grew alongside the name, so the two shared the spare
     width and the name truncated at "Above TEK, Table..." with half the row empty. Everything
     except the name is now sized to its content, which hands all of the slack to the one
     thing that can use it, and the ellipsis only appears when the name genuinely does not
     fit.
     THE X GOES LAST, at the far end of the row. It was directly after the name, which put a
     dismiss control in the middle of the line between the name and the stepper. */
  .checkout-item .co-qty { flex: 0 0 auto; justify-content: flex-start; order: 1; }
  .checkout-item .co-line { flex: 0 0 auto; text-align: right; order: 2; }
  .checkout-item .co-remove { order: 3; flex: 0 0 auto; margin-left: auto; padding-left: .6rem; padding-right: 0; }

  /* THE BASKET NOW MATCHES THE CHECKOUT. The note above used to say the basket was
     deliberately left alone because it read fine as a row; with the two panels doing the
     same job either side of the same order, they are better the same shape. Every rule
     below is the checkout's, applied to the basket's own class names. */
  .basket-list { display: block; }
  .basket-item {
    display: flex; flex-wrap: nowrap; justify-content: flex-start; align-items: center;
    gap: .55rem .9rem; text-align: left;
  }
  /* Same two lines as the checkout item above, and for the same reasons: name and × on the
     first, stepper on the second, name truncating so it can never make a third. */
  .basket-item .basket-item-info { flex: 1 1 auto; min-width: 0; text-align: left; }
  .basket-item .basket-item-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* Type and price come out on a PHONE only. Stacked, every item was already four lines
     deep before the meta line was added to it, and the basket is a running tally at that
     size. With the row back on a desktop there is a whole column doing nothing, so the
     information goes back where there is room for it. */
  .basket-item .basket-item-meta { display: none; }
  .basket-item .basket-qty { flex: 0 0 auto; justify-content: flex-end; order: 1; }
  .basket-item .basket-remove { order: 2; flex: 0 0 auto; margin-left: auto; padding-left: .6rem; padding-right: 0; }

  /* THE DATE CONTROLS CENTRE WITH EVERYTHING ELSE (Cam, 24 Aug). Start date and hire
     length are built as left-aligned columns with a row that wraps, and .co-mode-wrap is
     pushed to the right on a margin-left: auto. On a 298px screen every one of those wraps
     onto its own line anyway, so the left edge was the only thing holding them together
     while the items above them had already gone centred. All four axes are set here: the
     stack, the row, each label's own column, and the mode control's auto margin, which has
     to be released or it stays hard right on a line of its own. */
  /* LEFT, NOT CENTRED (Cam, 10 Sep: "add a start date and hire length for a price to be left
     aligned"). The note above is 24 Aug reasoning from when the ITEMS above this block were
     centred and the left edge was the only thing left holding the date controls together.
     The items are left aligned now, and so is everything else in this panel, so centring this
     one block was the only thing off the axis. */
  .checkout-dates { align-items: center; text-align: center; }
  /* THE PROMPT STAYS ON A PHONE. It was briefly hidden here, when the option beside it was a
     boxed control taking a line of its own; as two lines of text centred under Back to
     basket it costs almost nothing and keeps exact dates reachable at every width, which is
     what Cam meant by "so still keeps the option". */
  .checkout-dates-row { justify-content: center; }
  .checkout-dates label, .co-length, .co-mode-wrap { align-items: flex-start; }
  /* THE MODE TOGGLE DROPS TO A LINE OF ITS OWN (Cam, 24 Aug). It shares the wrapping row
     with the date fields, so in exact-dates mode the row holds three things, From, To and
     "Use hire length", and centring the row centres all three together: the From/To pair
     ends up sitting left of the middle with the toggle making up the width on the right.
     A full-width row of its own takes it out of that measurement, so From and To are the
     only two things being centred and they land on the middle of the panel.
     Both modes, not just this one: hire length is a wide control and the row is no less
     crowded with it there. Releasing the auto margin as well, or the toggle stays hard
     right on its new line instead of centring on it. */
  .co-mode-wrap { flex: 0 0 100%; margin-left: 0; }
  /* The empty label above the button is a spacer that lines it up with the fields when it
     is beside them. On its own line it is just a blank line above the control. */
  .co-mode-wrap .co-date-label { display: none; }
}
.co-name { min-width: 0; }
/* Category and rate stay HERE, at the checkout, and come out of the basket instead. The
   basket is a running tally you glance at while still browsing, so a type label and a
   per-day figure under every name is more than it needs; the checkout is where the order
   is actually read back, and that is the place for both. */
.co-type { display: block; color: var(--mid-grey); font-size: var(--fs-body); margin-top: 2px; }
.co-qty { display: inline-flex; align-items: center; gap: .5rem; color: var(--ink); white-space: nowrap; }
.co-qty-btn { position: relative; width: 1.55rem; height: 1.55rem; flex: none; border: 2px solid var(--ink); border-radius: 0; font-size: 0; line-height: var(--lh-flat); color: var(--ink); background: transparent; transition: background .2s var(--ease), color .2s var(--ease); }
/* Hover only where there is a pointer, the same reason as the basket + in the header: on a
   phone :hover latches on tap, so the last stepper touched stayed filled in as though it
   were selected. */
@media (hover: hover) and (pointer: fine) {
  .co-qty-btn:hover { background: var(--ink); color: #fff; }
}
/* +/- drawn with centred bars so they never depend on glyph metrics */
.co-qty-btn::before, .basket-qty-btn::before, .co-step::before { content: ""; position: absolute; top: 50%; left: 50%; width: 10px; height: 2.5px; transform: translate(-50%, -50%); background: currentColor; }
.co-qty-btn[data-co-inc]::after, .basket-qty-btn[data-inc]::after, .co-step[data-inc]::after { content: ""; position: absolute; top: 50%; left: 50%; width: 2.5px; height: 10px; transform: translate(-50%, -50%); background: currentColor; }
.co-qty-n { min-width: 1.3ch; text-align: center; }
/* Pulled in from the right so the glyph sits inside the row's rule. Its box used to end
   exactly where the rule ends, and with no space of its own the × read as hanging off the
   end of the stroke rather than sitting within it. */
/* Yellow is stated HERE, once, rather than left to a second .co-remove further down the
   file overriding a grey set here. That arrangement is what hid the same fix on
   .basket-remove: the override looked right, and a later declaration quietly won. One
   declaration, no ordering to keep track of, and no breakpoint at which it can differ. */
.co-remove { flex: none; font-size: 1.4rem; line-height: var(--lh-flat); color: var(--yellow); padding-left: .3rem; padding-right: .5rem; transition: color .2s var(--ease); }
/* No hover darkening. There used to be a pair of these, this one and a yellow one in the
   checkout's own block below, and the later yellow rule was what actually applied. With
   the × now full yellow at rest the yellow hover went with it, which left THIS rule as the
   only one still standing: hovering the × turned it dark ink on a grey panel. A control
   that is already at full strength has nothing to gain by being pointed at. */
.co-rate { color: var(--mid-grey); white-space: nowrap; text-align: right; }
.co-line { white-space: nowrap; text-align: right; }
/* NO RULE ABOVE THE TOTALS (Cam, 10 Sep: "there is a line underneath the equipment list on
   the checkout that needs removing"). The items list closes itself now, so a border here was
   a second line a few pixels under the table's own, reading as a stray. The margin is the
   break instead. */
.checkout-totals { border-top: 0; margin-top: 1.1rem; padding-top: 0; }
.co-subtotal { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--fs-body); }
.co-deposit { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--fs-12); color: var(--mid-grey); margin-top: .5rem; }
.checkout-note { color: var(--mid-grey); font-size: var(--fs-12); line-height: 1.5; margin-top: 1rem; max-width: 60ch; }
/* checkout enquiry form — light fields for the white panel (site default fields are dark) + space before the item list */
.checkout-form { margin: 1.4rem 0 2.4rem; }
.checkout-form label { color: var(--mid-grey); }
.checkout-form input, .checkout-form select, .checkout-form textarea { height: 44px; background-color: var(--white); color: var(--ink); border: 2px solid var(--ink); }
.checkout-form textarea { height: 84px; }
.checkout-form input::placeholder, .checkout-form textarea::placeholder { color: var(--mid-grey); }
.checkout-form input:focus, .checkout-form select:focus, .checkout-form textarea:focus { outline: 2px solid var(--ink); }
.checkout-form select { background-image: linear-gradient(45deg,transparent 50%,var(--ink) 50%),linear-gradient(135deg,var(--ink) 50%,transparent 50%); }
.checkout-form select option { background: var(--white); color: var(--ink); }
.checkout-form input[type="date"]::-webkit-calendar-picker-indicator { filter: none; }
.checkout-disclaimer { font-size: var(--fs-12); color: var(--mid-grey); margin-top: 1rem; }
.checkout-newsletter { display: flex; align-items: center; gap: .55rem; font-size: var(--fs-12) !important; color: var(--mid-grey); margin-top: .9rem; cursor: pointer; }
.checkout-newsletter input[type="checkbox"] { width: 16px; height: 16px; min-width: 16px; background: var(--white); border: 2px solid var(--ink); accent-color: var(--ink); padding: 0; }
/* enquiry-sent confirmation — compact centred card, grey with a yellow outline */
/* The thank-you card, the third view. Centred by the panel's own auto margins now rather than
   by flex alignment, since there is no flex container around it any more. */
.checkout.checkout-confirmed { padding: 1rem 0; }
.checkout-confirmed .checkout-panel { max-width: 380px; width: 100%; border: 2px solid var(--yellow); }
.checkout-confirm { padding: 1.25rem .5rem; text-align: center; }
.checkout-confirm .header-1 { font-size: var(--fs-lead); color: var(--yellow); margin-bottom: .8rem; }
.checkout-confirm p { font-size: var(--fs-body); line-height: var(--lh-narrow); color: var(--yellow); margin: 0 auto 1.6rem; max-width: 24ch; }
.checkout-confirm .checkout-actions { justify-content: center; }
/* Done = yellow-outlined box, fills yellow with black text on hover */
.checkout-confirm .checkout-actions .button { background: transparent; color: var(--yellow); border-color: var(--yellow); }
.checkout-confirm .checkout-actions .button:hover { background: var(--yellow); color: #000; border-color: var(--yellow); }
/* cookie consent — yellow box, bottom-left */
.cookie-consent { position: fixed; left: clamp(1rem, 3vw, 2rem); bottom: clamp(1rem, 3vw, 2rem); z-index: 70; width: min(420px, calc(100vw - 2rem)); background: var(--yellow); color: var(--ink); padding: 1.75rem; }
.cookie-title { font-size: var(--fs-title); margin-bottom: .6rem; }
.cookie-text { font-size: var(--fs-12); line-height: 1.55; margin-bottom: 1.25rem; }
.cookie-actions { display: flex; flex-wrap: wrap; gap: .55rem; }
/* THREE BUTTONS, ONE LINE, DOWN TO THE SMALLEST PHONE (Cam, 19 Aug: on a 12 mini "Accept
   all" dropped to a line of its own). Wrapping is what orphans it, so wrapping is turned
   off and the three share the width instead: an equal share each, padding closed up, and
   labels held on one line so a button shrinks rather than breaking its own words. */
@media (max-width: 430px) {
  .cookie-actions { flex-wrap: nowrap; gap: .4rem; }
  .cookie-actions .cookie-btn { flex: 1 1 0; min-width: 0; padding: 9px 6px; white-space: nowrap; text-align: center; }
}
.cookie-btn { font-size: var(--fs-12); line-height: var(--lh-flat); padding: 9px 14px; border: 2px solid var(--ink); background: var(--ink); color: var(--yellow); cursor: pointer; text-decoration: none; transition: background-color .18s var(--ease), color .18s var(--ease); }
.cookie-btn.ghost { background: transparent; color: var(--ink); }
.cookie-btn.ghost:hover { background: var(--ink); color: var(--yellow); }
.cookie-btn:hover { background: transparent; color: var(--ink); }
/* cookies page — grey background with yellow text (test) */
/* grey/yellow pages — cookies, newsletter, expanded newsletter */
/* The clearance for the fixed bar normally sits on body, which is fine while the page is
   white because body is white too. Here the colour is on main, so that padding showed as
   a white strip between the bar and the grey, at every width. Move the clearance onto
   main itself and the grey runs up under the bar with nothing leaking through. */
/* CATALOGUE VIEW TAKES THE PAGE GREY (Cam, 23 Aug 2026). Same field as News and About, so
   the index of every product reads as its own place rather than as a panel dropped into the
   white Hire page. Everything outside the catalogue turns with it: the title, the standfirst
   and the breadcrumb trail. Reusing .grey-page's own declarations would have meant adding a
   class to <body> that also brings its padding and legal-page rules, so the colours are
   restated here and nothing else comes with them. */
/* Fades both ways. The colour change is a transition on the base selectors, not on
   .catalogue-open, or it would ease in when the class is added and snap back when it is
   removed. The catalogue itself fades in with pageIn; the browse grid does the same coming
   back, so neither view appears at full strength in a single frame. */
body[data-page="equipment"],
body[data-page="equipment"] main,
body[data-page="equipment"] main * {
  transition: background-color .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease);
}
/* ANIMATION REMOVED 26 AUG. setCatalogue now goes through swap(), which fades the outgoing
   section out and the incoming one in on its own .fade-swap / .fade-in classes. Leaving this
   here meant the arriving section ran pageIn AND swap's fade-in at the same time, two opacity
   curves of different lengths over the same content, which is exactly the double-flash the
   note at @keyframes pageIn describes. One arrival, not two. */
/* THE PAGE NO LONGER CHANGES COLOUR (Cam, 10 Sep: "I don't like the fact that there is a
   delay on animations when the thing loads out, the background goes grey to white and I see
   the boxes").
   Every selector below used to be `body.catalogue-open main ...`, recolouring the WHOLE page
   grey with yellow type, which was right when the catalogue replaced the grid in the flow and
   the page WAS the catalogue. Since it became a panel that covers the page, that recolour
   only ever showed on the way out: the panel slid up over a page still grey, then the class
   came off and the 0.35s transition on body[data-page="equipment"] ran it back to white with
   the card tiles arriving underneath. That is the grey-to-white and the boxes.
   Scoped to the panel instead. The page keeps its own colours throughout and never
   transitions, so closing is the panel sliding up over a page that has not moved or changed,
   and there is nothing left to flash. */
.catalogue-panel,
.catalogue-panel h1, .catalogue-panel h2, .catalogue-panel h3,
.catalogue-panel p, .catalogue-panel li, .catalogue-panel a,
.catalogue-panel button, .catalogue-panel .sub,
/* spans explicitly: the catalogue's rows are two spans inside a button, and .cat-price sets
   its own colour, so neither inherits from the button above it. */
.catalogue-panel span, .catalogue-panel .cat-price,
.catalogue-panel .cat-name,
.catalogue-panel .breadcrumbs,
.catalogue-panel .breadcrumbs a,
.catalogue-panel .breadcrumbs span { color: var(--yellow); }
/* DOUBLED CLASS, for specificity. `.cat-group h3` sets `border-bottom: 2px solid var(--ink)`
   at (0,2,0), so a plain `.catalogue-panel *` at (0,1,0) lost to it and the group headings
   kept a black rule on a grey panel. Repeating the class takes this to (0,2,0) and later in
   the file wins the tie. Same trick .basket-panel already uses on its buttons. */
.catalogue-panel.catalogue-panel * { border-color: var(--yellow); }
/* The back control is a bordered button on white; on grey it reads as yellow on yellow
   unless the fill is dropped too. */
.catalogue-panel .button { background: transparent; color: var(--yellow); border-color: var(--yellow); }
.catalogue-panel .button:hover { background: var(--yellow); color: var(--ink); }
/* NO TRANSITION INSIDE THE PANEL. The rule on body[data-page="equipment"] above puts a .35s
   ease on background, colour and border for every element in main, which is what made the
   old in-flow swap fade. The panel arrives on a transform with its colours already correct,
   so the same transition here would fade its type up from the page's black after it had
   finished sliding. */
/* DOUBLED, for specificity, and this is the same fight the transform below has to win.
   `body[data-page="equipment"] main *` up the file is (0,1,2) and puts a .35s ease on
   background, colour and border for everything inside main. A plain `.catalogue-panel *` is
   (0,1,0) and loses to it, so the panel's type kept fading from the page's black after the
   banner had already arrived. */
.catalogue-panel.catalogue-panel, .catalogue-panel.catalogue-panel * { transition: none; }

body.grey-page { padding-top: 0; }
body.grey-page main { padding-top: calc(max(72px, var(--header-h, 72px)) + 24px); }
body.grey-page main { background: var(--grey); font-family: var(--font); }
body.grey-page:not([data-page="journal"]) main, body.grey-page:not([data-page="journal"]) main h1, body.grey-page:not([data-page="journal"]) main h2, body.grey-page:not([data-page="journal"]) main h3,
body.grey-page:not([data-page="journal"]) main p, body.grey-page:not([data-page="journal"]) main li, body.grey-page:not([data-page="journal"]) main a, body.grey-page:not([data-page="journal"]) main strong,
body.grey-page:not([data-page="journal"]) main .sub, body.grey-page:not([data-page="journal"]) main .meta, body.grey-page:not([data-page="journal"]) main .date, body.grey-page:not([data-page="journal"]) main .title,
body.grey-page:not([data-page="journal"]) main .breadcrumbs, body.grey-page:not([data-page="journal"]) main .breadcrumbs span, body.grey-page:not([data-page="journal"]) main .breadcrumbs a { color: var(--yellow); }
body.grey-page:not([data-page="journal"]) main * { border-color: var(--yellow); }
/* Rules are drawn as a background, not a border, so the blanket border-color above never
   reached them and they stayed pale grey on the grey. Every line on a journal page is
   yellow: the divider above the back link, and any hr in the body copy. */
body.grey-page:not([data-page="journal"]) main .rule,
body.grey-page:not([data-page="journal"]) main hr { background: var(--yellow); border: 0; }
body.grey-page main img { border: 0; outline: 0; }

/* ── NEWS PAGES, 24 Aug 2026, Cam's call ──────────────────────────────────────
   The article reads on WHITE with BLACK text. Only the .page-head band keeps the
   grey-and-yellow treatment, alongside the site header, which is unchanged.
   This is scoped to [data-page="journal"], which is exactly the five news pages,
   so About, Cookies and the rental agreement keep the full-grey treatment above.
   NOTE what this frees: journal body copy no longer sits on --grey, so --grey is
   no longer capped by the 4.50:1 yellow-body-copy rule. The remaining yellow-on-
   grey is the site header and this band. */
body[data-page="journal"] main { background: var(--white); }
body[data-page="journal"] main,
body[data-page="journal"] main p, body[data-page="journal"] main li,
body[data-page="journal"] main h1, body[data-page="journal"] main h2,
body[data-page="journal"] main h3, body[data-page="journal"] main strong,
body[data-page="journal"] main a, body[data-page="journal"] main .title,
body[data-page="journal"] main .sub, body[data-page="journal"] main .meta,
body[data-page="journal"] main .date { color: var(--ink); }
body[data-page="journal"] main * { border-color: var(--line); }
body[data-page="journal"] main .rule, body[data-page="journal"] main hr { background: var(--line); border: 0; }

/* The head band keeps grey and yellow, and needs its own padding now that it is a
   band on white rather than the top of one continuous grey field. */
/* main carries the fixed-header offset as padding-top. That was invisible while main was
   grey, but on white it showed as a white bar above the head band. So on news pages the
   offset moves onto .page-head itself, and the grey runs right up under the header. */
body[data-page="journal"] main { padding-top: 0; }
/* THE NEWS INDEX HAS NO .page-head, SO NOTHING WAS CLEARING THE BAR (Cam, 24 Aug).
   The rule above moves the fixed-header offset off main and onto .page-head, which is
   right for the four article pages: they each have one, and it lets the grey run up under
   the bar with no white strip above it. journal.html is the odd one out. Its head is
   .journal-head-card, the first cell of the grid rather than a band above it, so it has
   no .page-head to take the offset, main's padding was cancelled, and the breadcrumbs and
   the News title sat underneath the header.
   Matched on the absence of the band rather than on the page, so any future news page
   built the same way is covered and the article pages are untouched. */
body[data-page="journal"] main:not(:has(.page-head)) {
  padding-top: calc(max(72px, var(--header-h, 72px)) + 24px);
}
/* The .page-head rule that stood here is gone (10 Sep 2026). Every one of its three
   properties, background, padding-top and padding-bottom, was overridden by the later
   .page-head rule further down, so the whole rule was dead: the head has been transparent
   rather than grey, and on the later padding, for as long as both have existed. */
body[data-page="journal"] .page-head,
body[data-page="journal"] .page-head h1, body[data-page="journal"] .page-head .sub,
body[data-page="journal"] .page-head .meta, body[data-page="journal"] .page-head .date,
body[data-page="journal"] .page-head .title, body[data-page="journal"] .page-head a,
body[data-page="journal"] .page-head .breadcrumbs,
body[data-page="journal"] .page-head .breadcrumbs span,
body[data-page="journal"] .page-head .breadcrumbs a { color: var(--yellow); }
body[data-page="journal"] .page-head * { border-color: var(--yellow); }
body.grey-page .page-head h1 { margin-bottom: 1.1rem; }
body.grey-page .legal .toc { background: var(--grey); border-color: var(--yellow); }
body.grey-page .legal .toc, body.grey-page .legal .toc * { font-family: var(--font) !important; font-weight: 400; }
body.grey-page .legal .toc a, body.grey-page .legal .toc p, body.grey-page .legal .toc li { color: var(--yellow); }
/* home 'Recent newsletters' block — grey band with yellow text */
.newsletter-block { background: var(--grey); }
.newsletter-block .header-1, .newsletter-block a, .newsletter-block .u, .newsletter-block p, .newsletter-block .date, .newsletter-block .title { color: var(--yellow); }
.newsletter-block .news-card { border-color: var(--yellow); }
.newsletter-block .news-card-media { border: 2px solid var(--yellow); }
/* grey-page CTA band: heading black, button white */
body.grey-page .cta-band .header-1 { color: var(--ink) !important; }
/* Buttons on the grey pages sit ON the grey: yellow stroke, yellow type, no fill, and
   they fill yellow with black type when pointed at. One button behaviour on these pages,
   whether it is the quote button at the end of a journal piece or a CTA band. */
body.grey-page .button,
body.grey-page .cta-band .button {
  background: var(--grey); color: var(--yellow); border-color: var(--yellow);
}
body.grey-page .button:hover, body.grey-page .button:focus-visible,
body.grey-page .cta-band .button:hover {
  background: var(--yellow); color: #000; border-color: var(--yellow);
}
/* THE NEWS PAGES ARE WHITE NOW, SO THEIR BUTTONS ARE TOO (Cam, 24 Aug). The rule above is
   written for a page whose ground is Kitto grey, and it was right when every grey-page was
   one continuous grey field. The news pages have since gone white, main and all, but they
   are still .grey-page, so their buttons kept painting themselves grey with yellow type
   and sat on the white as a leftover panel from the old scheme.
   Black stroke, black type, no fill: the button is drawn in the same ink as the writing
   around it and reads as part of the page rather than as a coloured object on it. Filling
   to solid ink with white type on hover, which is the base .button's own behaviour run
   backwards, so a news page button still answers the pointer the way every other one does.
   Placed AFTER the grey-page rules deliberately: `body[data-page="journal"] .button` and
   `body.grey-page .button` have identical specificity, so source order is the only thing
   deciding this, and moving it up the file would silently undo it. */
body[data-page="journal"] .button,
body[data-page="journal"] .cta-band .button {
  background: var(--white); color: var(--ink); border-color: var(--ink);
}
body[data-page="journal"] .button:hover, body[data-page="journal"] .button:focus-visible,
body[data-page="journal"] .cta-band .button:hover {
  background: var(--ink); color: var(--white); border-color: var(--ink);
}

/* ═══ checkout — yellow throughout (retained Kitto brand scheme) ═══
   No background of its own any more: it sits inside the basket panel, which is already this
   grey. Setting it here as well meant two elements painting the same colour, and the day one
   of them changed they would have come apart. */
.checkout-panel { color: var(--yellow); }
.checkout-close { color: var(--yellow); border: 0; }
/* Italic on hover, matching "set exact dates" below it. No fill: there is no box left to fill,
   and a block of yellow behind two words would be the only solid on a panel of rules. */
.checkout-close:hover, .checkout-close:focus-visible { background: transparent; color: var(--yellow); font-style: italic; }
.checkout-head .header-1, .checkout-duration, .checkout-note, .checkout-disclaimer,
.co-name, .co-qty, .co-line, .co-subtotal, .checkout-dates label { color: var(--yellow); }
.co-type, .co-rate, .co-deposit { color: rgba(255,237,20,.72); }
/* border-color, not border-bottom-color: these are boxes now, so all four sides take it. */
.checkout-dates input[type="date"] { color: var(--yellow); border-color: var(--yellow); }
/* A YELLOW CALENDAR, DRAWN (Cam, 10 Sep: "can we make the calendar icon on set exact dates
   yellow"). The browser's own indicator is a black glyph and the only way to recolour it is a
   filter, which can reach white, what invert(1) brightness(2) was doing, but cannot reach a
   specific hue without a long unreadable chain of hue-rotate and saturate guesses that drifts
   with every browser.
   So the glyph is replaced outright with an inline SVG on the brand yellow. It is exact, it
   is legible in the file, and it will not shift when Chrome changes its own icon. Same two
   tabs and rule as a calendar anywhere, on the 2px stroke the site's other drawn icons use. */
.checkout-dates input[type="date"]::-webkit-calendar-picker-indicator {
  filter: none; opacity: 1; cursor: pointer;
  width: 15px; height: 15px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FFED14' stroke-width='2' stroke-linecap='butt'%3E%3Crect x='3' y='5' width='18' height='16'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
  background-size: contain; background-repeat: no-repeat; background-position: center;
}
.checkout-dates .co-date-label { color: var(--yellow); }
.co-step { border-color: var(--yellow); color: var(--yellow); }
.co-mode-toggle { color: var(--yellow); }
@media (hover: hover) and (pointer: fine) {
  .co-step:hover { background: var(--yellow); color: #4a4a4a; }
}
.co-length-controls { border-color: var(--yellow); }
#checkoutQty { color: var(--yellow); border-right-color: var(--yellow); }
#checkoutUnit { color: var(--yellow); border-color: var(--yellow); background-image: linear-gradient(45deg,transparent 50%,var(--yellow) 50%),linear-gradient(135deg,var(--yellow) 50%,transparent 50%); }
#checkoutUnit option { background: var(--grey); color: var(--yellow); }
.co-saving { color: rgba(255,237,20,.72); }
.checkout-form label { color: var(--yellow); }
.checkout-form input, .checkout-form select, .checkout-form textarea { background-color: transparent; color: var(--yellow); border-color: var(--yellow); }
.checkout-form input::placeholder, .checkout-form textarea::placeholder { color: rgba(255,237,20,.5); }
.checkout-form input:focus, .checkout-form select:focus, .checkout-form textarea:focus { outline-color: var(--yellow); }
.checkout-form select { background-image: linear-gradient(45deg,transparent 50%,var(--yellow) 50%),linear-gradient(135deg,var(--yellow) 50%,transparent 50%); }
.checkout-form select option { background: var(--grey); color: var(--yellow); }
.checkout-item { border-top-color: rgba(255,237,20,.28); }
.checkout-item:first-child { border-top-color: var(--yellow); }
.co-qty-btn { border-color: var(--yellow); color: var(--yellow); }
@media (hover: hover) and (pointer: fine) {
  .co-qty-btn:hover { background: var(--yellow); color: #4a4a4a; }
}
/* THE × IS ALWAYS FULLY YELLOW (Cam, 24 Aug). It was held at 70% and only came up to full
   on hover, which on a phone never happens: the one control for taking something back out
   of the order sat permanently dimmed and read as disabled rather than as available.
   Removing an item is not a secondary action, so it is not drawn as one. */
/* .co-remove is set yellow at its own declaration further up, not here. */
/* THE ERROR STATE OF THE STANDING NOTE, 10 Sep 2026.
   The message now replaces `All enquiries are subject to approval.` in the line under the
   buttons, rather than appearing in its own paragraph above them. Same slot, so nothing
   moves when it appears, and it is beside the control that was pressed.
   Set as a yellow block with ink type, which is the site's own answer to putting something
   on the grey: the tile captions and the journal pictures already do it. On the panel the
   note is otherwise small grey text, which measured 1.82:1 against the ground and was why
   pressing Send enquiry read as the button doing nothing. This is 15.2:1. */
.checkout-note.is-error {
  color: var(--ink); background: var(--yellow);
  font-size: var(--fs-15); line-height: var(--lh-narrow);
  padding: .5rem .7rem;
}
/* newsletter opt-in — custom checkbox matching the yellow form controls */
.checkout-newsletter { color: var(--yellow); }
.checkout-newsletter input[type="checkbox"] { appearance: none; -webkit-appearance: none; width: 16px; height: 16px; min-width: 16px; margin: 0; padding: 0; border: 2px solid var(--yellow); background: transparent; border-radius: 0; display: inline-grid; place-content: center; cursor: pointer; flex: none; }
.checkout-newsletter input[type="checkbox"]::before { content: ""; width: 8px; height: 8px; background: var(--yellow); transform: scale(0); transition: transform .12s var(--ease); }
.checkout-newsletter input[type="checkbox"]:checked::before { transform: scale(1); }
.checkout-panel .button { background: transparent; color: var(--yellow); border-color: var(--yellow); }
.checkout-panel .button:hover { background: var(--yellow); color: #000; }
.checkout-panel .button.ghost { background: transparent; color: var(--yellow); border-color: var(--yellow); }
.checkout-panel .button.ghost:hover { background: var(--yellow); color: #000; }

.checkout-actions { display: flex; align-items: center; justify-content: flex-start; gap: 1.25rem; margin-top: 1.75rem; flex-wrap: wrap; }
.checkout-actions .button { font-size: var(--fs-body); }
body.checkout-open { overflow: hidden; }

/* ── Expanded product card: grey and yellow, matching the basket and checkout ──────
   No image here any more; product photos live on the grid thumbnails. That makes the
   card a single column of specs, so it is narrower and the panel drops the split. */
.kit-modal-media { display: none !important; }
.kit-modal-panel { grid-template-columns: 1fr !important; background: var(--grey); color: var(--yellow); }
.kit-modal-body { overflow-y: auto; max-height: 90vh; }
.kit-modal-close { background: transparent; color: var(--yellow); }
.kit-modal-close:hover { background: var(--yellow); color: var(--grey); }
.kit-modal-cat, .kit-modal-title, .kit-modal-price,
.kit-modal-desc, .kit-modal-desc p, .kit-modal-overview li { color: var(--yellow); }
.kit-modal-overview li::before { color: rgba(255,237,20,.6); }
.kit-modal-specs { border-top-color: rgba(255,237,20,.35); }
.kit-modal-specs > div { border-bottom-color: rgba(255,237,20,.35); }
.kit-modal-specs dt { color: rgba(255,237,20,.72); }
.kit-modal-specs dd { color: var(--yellow); }
/* The base .button is filled black, which sat as a dead black slab on the grey panel.
   Outlined yellow at rest, filling yellow with black text on hover. */
.kit-modal-panel .button {
  background: transparent; border-color: var(--yellow); color: var(--yellow);
}
.kit-modal-panel .button:hover {
  background: var(--yellow); border-color: var(--yellow); color: var(--ink);
}
.kit-modal-panel .u { color: var(--yellow); }

/* Two columns held as long as they are worth having, which is the point of the block: the
   claim on the left, the evidence on the right. Stacking is the last resort.

   Nothing here sets a scale any more. The logos size themselves as a fraction of their cell
   (see .partner-logo-item img), so they shrink together as the column narrows and stay in
   the proportion they were tuned to. All these bands decide is HOW MANY COLUMNS.

   Nothing here sets a scale any more, and nothing here sets a row gap. Both live once on
   .partnership-logos and apply at every width, so the marks are the same size and the same
   distance apart whatever the layout does. All these bands decide is HOW MANY COLUMNS, and
   they are chosen so the widest mark always fits its cell at the tuned size.

   1151 and up   two columns, logos four across.
   801 to 1150   two columns, logos three across. Four across was the band where the marks
                 came out SMALLEST, 114px cells against the 137 Cromwell Place needs.
   600 to 800    two columns, logos two across.
   464 to 599    stacked, three across, and "Learn about KitMapper" drops underneath.
   463 and down  stacked, two across. Three no longer fits at the tuned size.  */

@media (max-width: 1150px) and (min-width: 801px) {
  /* THE GUTTER REOPENS (Cam, 19 Aug, looking at 980: "Learn about KitMapper" sat too close
     to the paragraph beside it). It was 1.5rem with a hard 1fr 1.8fr split, and both of
     those existed for a reason that has since gone: when the logos were four across and
     sized as a fraction of their cell, the right column's width was the ONLY thing setting
     how big the marks were drawn, so every pixel had to be pushed that way. The marks are
     now a fixed height in content-sized columns, so the right column only has to be wide
     enough to FIT them, not to inflate them.
     What it has to fit is 414px, the three columns of marks plus their gaps at the tuned
     scale. 1fr 1.5fr with this gutter clears that across the whole band: about 428px of
     logo column at 801 and 530px at 980, while the paragraph keeps 285px and 353px, which
     at --lh-narrow is 31 and 39 characters a line. */
  .partnership-inner { gap: clamp(2.5rem, 5vw, 4rem); grid-template-columns: 1fr 1.5fr; }
  /* THREE ACROSS, not four. Four across is what made this the band where the marks came
     out smallest: at 810 the cells were 114px and Cromwell Place needs about 137 at the
     tuned size, so max-width clipped it and the whole set drew small. Three across gives
     154px at the same width, which clears it, at the cost of one more row. */
  .partnership-logos { grid-template-columns: repeat(3, auto); }
}

@media (max-width: 800px) and (min-width: 600px) {
  /* Back to 1.25fr once the logos are two across: they no longer need the extra width, and
     the text does, since the row is down to about 630px here. */
  .partnership-inner { gap: 1.5rem; grid-template-columns: 1fr 1.25fr; }
}

@media (max-width: 800px) and (min-width: 600px) {
  /* Two across, four rows. Both columns align left and each takes the same pull: over two
     columns, leading one and centring the other reads as one column adrift. */
  /* Two across. Size and gaps come from the base rule, as they do in every band. */
  .partnership-logos { grid-template-columns: repeat(2, auto); max-width: none; }
}

@media (max-width: 599px) {
  /* Stacked, and the one width where spreading the logos is right: three across, edge to
     edge, because packing marks to the left of a 390px screen leaves a column of nothing
     beside them. Every cell aligns left even so: leading the row and centring the rest
     measured as five different left edges across three columns, since no two of the eight
     logos are the same width. Three columns should give three edges.
     Rows tighten, and the stacked gap comes down: 1.5rem left too much air between the
     paragraph and "Learn about KitMapper" for two things that belong to each other. */
  .partnership-inner { grid-template-columns: 1fr; gap: 1.1rem; }
  .partnership-logos { grid-template-columns: repeat(3, auto); max-width: none; }
  .partnership-logos .partner-logo-item { padding: 4px 0; justify-content: flex-start; }
}
/* Two across below 464, because three stops fitting there and the marks would start
   shrinking again, which is the whole thing this block is trying to avoid. Cromwell Place
   is 137px wide at the tuned scale; three cells plus their gaps need 432px of content, and
   the page keeps 16px either side, so 464 is where it runs out. Below that, two across
   gives 174px cells at 390 and every mark stays exactly the size it is on desktop. */
@media (max-width: 463px) {
  .partnership-logos { grid-template-columns: repeat(2, auto); }
}

/* ═══════════════ REQUEST A QUOTE, SAME CALL AS THE PARTNERSHIP BLOCK ═══════════════
   Two columns down to 500 rather than stacking at 992: at 650 there is still room for the
   intro and contact details beside the form. */

@media (max-width: 992px) and (min-width: 768px) {
  /* The 5rem gutter is what made this impossible low down, so it closes to 1.5rem, and the
     split moves from 1fr 1.3fr to 1fr 1.25fr. At 768 that is roughly 309px of intro against
     387px of form, which both carry. */
  .form-inner-wrap { grid-template-columns: 1fr 1.25fr; column-gap: 1.5rem; row-gap: 2rem; }
}

/* THE FORM DROPS UNDER THE TEXT AT 768, NOT AT 500 (Cam, 19 Aug).
   It used to hold two columns all the way down to 500, where the intro was about 197px
   against 247px of form: an intro running at roughly twenty characters a line, beside
   fields too narrow to type a venue address into. Two columns that narrow are worse than
   one of a decent width.
   768 is where the form's own fields already go single column on their own rule, so the
   whole block now changes shape at one width instead of two, and the intro keeps the full
   measure with the form underneath it. */
@media (max-width: 768px) {
  .form-inner-wrap { grid-template-columns: 1fr; }
}

/* ═══════════════════════════════════════════════════════════════════════════════════════
   HERO TILES ON A PHONE TOO (Cam, 26 Aug: "i wanted the tiling effect for mobile also")
   ═══════════════════════════════════════════════════════════════════════════════════════
   The phone hero has been a swipe strip since 19 Aug, on the reasoning noted up at the
   .hero-mosaic rule: a two-column mosaic at 390px turns the tiles into postage stamps.
   That reasoning is sound for TWO COLUMNS. It is not an argument against tiling as such.
   So the arrangement comes down to ONE column and the stagger moves into it: the tiles keep
   different widths, different proportions and alternating sides, and none of them is ever
   narrower than two thirds of the screen. That is the thing the desktop mosaic is doing,
   at a size a phone can hold, rather than a shrunken copy of the desktop grid.

   Written as a later block rather than edited into the one above so the strip is still
   there, intact and commented, if this turns out to be the wrong call. Same media query and
   same specificity, so source order is what decides.

   The five pictures now sit one above another, so everything the strip needed has to be
   undone: the flex row, both overflow axes, the snap, the peek width and the trailing
   margin that held the last tile off the edge. */
@media (max-width: 768px) {
  /* The strip bled off the right of the screen on purpose, so the band had no right inset.
     A stacked tile has to sit inside the margin on both sides. */
  body.home .hero-band { padding-right: var(--hero-inset); }
  .hero-mosaic {
    display: block; height: auto;
    overflow: visible;
    scroll-snap-type: none;
  }
  .hero-col { display: contents; }
  .hero-tile {
    flex: none; width: auto;
    margin: 0 0 var(--hero-gap) 0;
    scroll-snap-align: none;
    aspect-ratio: auto;              /* each tile states its own below */
  }
  /* NOT `.hero-tile:last-child` (fixed 10 Sep). `.hero-col { display: contents }` above
     dissolves the two columns for LAYOUT but not for the DOM, so :last-child matches the
     last tile in EACH column, which since Prinks moved into the left one is Prinks as well
     as Sophie Clements. Prinks was getting `margin-right: 0` from it, a leftover from when
     this block was a horizontal swipe strip and the last tile had to clear the screen edge,
     and with `margin-left: auto` on the other side the auto then took ALL the free space and
     shunted it right while everything else sat centred.
     The right margin reset goes entirely, there is no horizontal strip left to clear, and
     only the genuinely final picture loses its bottom gap. */
  .hero-col-right .hero-tile:last-child { margin-bottom: 0; }

  /* THE STAGGER IS OFF ON A PHONE (Cam, 10 Sep: "staggered view doesn't quite work for
     mobile with the images", and "I'd like the images to be centred ... and give it equal
     space between each image").
     What was here: six different widths, six different margin-lefts (0, 0, 42, 4, 20, 24)
     and six different bottom margins, all tuned in late August to stop a narrow column of
     pictures reading as an S. That worked when every tile was a CROP and the varied shapes
     were doing some of the work. It stopped working once each tile took its photograph's
     own aspect-ratio: the pictures now vary in height by themselves, and a stagger on top of
     that is two kinds of irregularity fighting each other down one column.
     So: one width, centred, one gap. The pictures are the variation now.
     THE RATIOS ARE THE FILES, exactly as on desktop, which is what fixes Sophie Clements.
     tile-e was 4/3 LANDSCAPE here, set when work-clements-1 was a landscape photograph; the
     picture is the upright CRT stack now and 4/3 was squashing it, which is Cam's "its not
     tall enough". 3/4 is the file.
     Every object-position in this block has gone with the crops that needed steering. */
  .hero-feature,
  .hero-tile.tile-a, .hero-tile.tile-b, .hero-tile.tile-c,
  .hero-tile.tile-d, .hero-tile.tile-e {
    /* WIDER (Cam, 10 Sep: "the images on the mobile hero can be wider"). 88% left a visible
       margin either side of every picture on a screen that has little width to give away; 95%
       keeps the centring readable as centring, which is what stops them reading as full-bleed,
       while handing most of that margin back to the work. */
    width: 95%;
    margin-left: auto; margin-right: auto;
    margin-bottom: var(--hero-gap);
  }
  .hero-feature     { aspect-ratio: 3 / 2; }   /* work-surel-1     2560x1709 */
  .hero-tile.tile-a { aspect-ratio: 4 / 5; }   /* work-lloyd-3     1600x2000 */
  .hero-tile.tile-b { aspect-ratio: 3 / 2; }   /* hero-mendel-1    2000x1333 */
  .hero-tile.tile-c { aspect-ratio: 3 / 2; }   /* hero-faramawy-1  2560x1707 */
  .hero-tile.tile-d { aspect-ratio: 3 / 2; }   /* hero-lapelyte-1  2560x1709 */
  .hero-tile.tile-e { aspect-ratio: 3 / 4; }   /* work-clements-1  1499x2000 */
}

/* ═══ NO FOOTER CAROUSEL ON A PHONE (Cam, 26 Aug) ═══
   Kit at work is sixteen slides you advance one arrow-press at a time, below the footer,
   at the bottom of a page that on a phone is already long. Nobody was getting there, and
   the hero is a wall of the same photographs now anyway.
   .slider-viewport, NOT .slider-block. The block also contains .footer-spacer, which
   despite the name is the actual yellow footer band carrying the policy links and the
   credit: hiding the block would take the site's footer off every phone. The arrows are
   inside the viewport, so they go with it. */
@media (max-width: 768px) {
  .slider-block .slider-viewport { display: none; }
}

/* ═══ THE CATALOGUE STARTS HIGHER (Cam, 26 Aug: "starts way too low, bring that up") ═══
   The catalogue lives inside an ordinary <section>, so it inherits `main > section
   { padding-block: var(--sec) }`, which is 87px at a laptop width. That is the right
   opening for a section that follows other content down a page. It is the wrong opening for
   a view that REPLACES the page: the search row is pinned directly above it, so those 87px
   are just grey between the bar and the first thing the reader opened the catalogue to see.
   Scoped to body.catalogue-open so ordinary sections on the same page keep their rhythm,
   and the bottom padding is left alone because the end of the list still wants air. */
body.catalogue-open main > section { padding-top: clamp(1rem, 2vw, 1.75rem); }

/* ═══════════════════════════════════════════════════════════════════════════════════════
   THE NEWS PAGES GO WHITE, LOSE THE LEAD PICTURE, AND SET THEIR HEAD TO THE MARGIN
   (Cam, 26 Aug)  ·  data-page="journal", which is the index and all four articles
   ═══════════════════════════════════════════════════════════════════════════════════════
   WHAT WAS ACTUALLY GREY. The body of these pages has been white since the 24 Aug pass:
   `body[data-page="journal"] main { background: var(--white) }` is already up at the top of
   that block. The only grey left was .page-head, deliberately painted as a masthead BAND on
   white, with yellow type on it and the fixed bar's clearance folded into its own padding
   so the grey could run right up under the header. That band is what goes here.
   Scoped on data-page rather than by taking .grey-page off the five <body> tags: that class
   also carries the font-family, the image border reset and the page-head spacing, and
   about, cookies and the rental agreement are .grey-page too and are not part of this. */
body[data-page="journal"] { background: var(--white); }

/* THE BAND GOES, THE PADDING STAYS. .page-head is the only thing clearing the fixed bar on
   an article page, because the rule above it sets `main { padding-top: 0 }` precisely so the
   grey could reach the header. Take the padding out with the colour and the breadcrumbs end
   up underneath the bar. The inner half of it comes down, though: that was the band's own
   depth, and without a band to fill there is no reason for it. */
body[data-page="journal"] .page-head {
  background: transparent;
  /* BROUGHT IN LINE WITH EVERY OTHER PAGE (Cam, 26 Aug: "in all instances"). The shared rule
     that tightened About and the category pages cannot reach here: on news the bar clearance
     lives on .page-head rather than on main, so this padding has to carry it AND the head's
     own opening in one value, and it was the only page left at 134px from the bar when the
     rest had come down to 38 to 54.
     Same two reductions as the shared rule, applied to the two halves of the calc: the
     clearance keeps 8px past the bar instead of 24, and the head's own opening halves. */
  padding-top: calc(max(72px, var(--header-h, 72px)) + 8px + clamp(10px, 1.25vw, 18px));
  /* padding-bottom removed 10 Sep 2026: it was set again, tighter, further down. */
}
/* Yellow type was legible because it sat on grey. On white it is close to invisible, so the
   head takes the same ink the article body already uses, and its rules the same --line. */
body[data-page="journal"] .page-head,
body[data-page="journal"] .page-head h1, body[data-page="journal"] .page-head .sub,
body[data-page="journal"] .page-head .date, body[data-page="journal"] .page-head .title,
body[data-page="journal"] .page-head a,
body[data-page="journal"] .page-head .breadcrumbs,
body[data-page="journal"] .page-head .breadcrumbs span,
body[data-page="journal"] .page-head .breadcrumbs a { color: var(--ink); }
/* The date and section line stays quieter than the title, which is what .meta does
   everywhere else on the site. */
body[data-page="journal"] .page-head .meta { color: var(--mid-grey); }
body[data-page="journal"] .page-head * { border-color: var(--line); }

/* THE LEAD PICTURE GOES, and only the lead. `.media-full` is used again further down some
   of these articles, where it is the article's own artwork and stays. The adjacent sibling
   is what separates the two: the hero is the .media-full that directly follows the head,
   and nothing else on the page can match that. */
body[data-page="journal"] .page-head + .media-full { display: none; }

/* HEAD TO THE MARGIN. Four declarations, because the centring is applied in four places up
   at `body[data-page="journal"] .page-head ...`: the container centres the text, the
   breadcrumbs centre themselves as a flex row, and the h1 and .sub are centred again by auto
   inline margins. Any one left in place holds part of the head in the middle of the page.
   IN LINE WITH THE BODY TEXT, NOT WITH THE PAGE GUTTER (Cam, 26 Aug). The first version of
   this put `margin-inline: 0` on the head's .article, which took the breadcrumbs, the date
   line and the title out to the container edge at 56px while the article body underneath
   them stayed in its centred 760px column starting at 176px. Two left edges 120px apart
   down one page.
   So the auto margins stay and only the CENTRING OF THE TEXT goes. The head now sits in
   exactly the same 760px column as the body, and every line on the page, title and
   paragraph alike, starts on the same axis. .page-head h1 keeps its own 30ch cap. */
body[data-page="journal"] .page-head .container { text-align: left; }
body[data-page="journal"] .page-head .breadcrumbs { justify-content: flex-start; }
body[data-page="journal"] .page-head h1,
body[data-page="journal"] .page-head .sub { margin-inline: 0; }

/* The listing block on the index is a grey band, and a grey band is the thing this change
   exists to remove. The yellow outline goes with it: that stroke was added to bound the GREY
   fields, so on white there is no field left for it to bound. */
body[data-page="journal"] .newsletter-block { background: transparent; border-color: transparent; }

/* ═══════════════════════════════════════════════════════════════════════════════════════
   STANDING RULE (Cam, 26 Aug): A CAPTION BLOCK TAKES ITS COLOUR FROM WHAT IS BEHIND THE
   PICTURE, NOT FROM THE PICTURE
   ═══════════════════════════════════════════════════════════════════════════════════════
   On a GREY ground a caption is a YELLOW block with BLACK type. That is the hero tiles, and
   it is right there: yellow is the loudest thing available against Kitto grey.
   On a WHITE ground it inverts, to a GREY block with YELLOW type. A yellow block on white
   has almost nothing holding it to the page, and black on yellow on white is three values
   fighting over one small label.
   Apply this to any caption or overlay added to a white page from here on, not just the
   ones below.
   Scoped to data-page="journal" because that is where every .ph and .media-full caption on
   the site currently lives, and those pages went white on 26 Aug. A caption on a white page
   elsewhere should be added to this selector rather than given its own rule. */
body[data-page="journal"] .article-body .ph .slider-cap,
body[data-page="journal"] .media-full .ph .slider-cap {
  background: var(--grey);
}
/* Every part of the credit, including the paragraph. The colour has to be re-stated on the
   p for the same reason the ink version above it does: the article's own paragraph rules
   are more specific than anything the block can pass down by inheritance. */
body[data-page="journal"] .article-body .ph .slider-cap,
body[data-page="journal"] .media-full .ph .slider-cap,
body[data-page="journal"] .article-body .ph .slider-cap .cap-sub,
body[data-page="journal"] .media-full .ph .slider-cap .cap-sub,
body[data-page="journal"] .article-body .ph .slider-cap p,
body[data-page="journal"] .media-full .ph .slider-cap p,
body[data-page="journal"] .article-body .ph .slider-cap em,
body[data-page="journal"] .media-full .ph .slider-cap em { color: var(--yellow); }

/* ═══ NEWS HEADINGS CARRY MORE ON A PHONE (Cam, 26 Aug) ═══
   The whole scale is fluid, so on a 390px screen --fs-statement resolves to its 27px FLOOR
   while --fs-body sits at a flat 18px. The title ends up only 1.5x the paragraph under it,
   and on a white page with the lead picture gone it is the only thing left doing the work
   of a masthead. Desktop is unaffected: there the same token is at 36px against 18px, which
   is the 2x this is reaching for.
   Steeper vw term rather than a bigger floor, so the size climbs through the phone range
   instead of sitting pinned at the bottom of it, which is the fault the home statement hit
   on 24 Aug. 8.5vw puts the title near 33px on a 390px screen and 36px on a 430px one.
   The in-article h2s move with it. They are --fs-title, a flat 1.5rem, so at body size 18px
   a section heading was only a third larger than the text it introduces and the article read
   as one undifferentiated column on a phone.
   ⚠️ THE h2 FLOOR MUST STAY AT OR ABOVE 1.5rem. First attempt used clamp(1.375rem, 5.4vw,
   1.75rem), and because --fs-title is a FLAT 24px rather than fluid, the 22px floor won at
   390px and the change made section headings SMALLER than they had been, 1.22x the body
   where they had been 1.33x. A fluid replacement for a fixed token has to start where the
   fixed one already was, or the phone end quietly goes backwards. */
/* The phone sizes that were here are gone (10 Sep 2026). They were overridden 480 lines
   further down, by the later pass that took both "one increment down the scale", so this
   block set 8.5vw and 7.4vw actually applied. Anyone reading it saw a size the page never
   used. The live rules are the ones near the ph-viewer and the article-body h2 blocks. */

/* ═══ THE SAME RULE, THE REST OF THE NEWS PAGES (Cam, 26 Aug) ═══
   Two more overlays were still yellow-and-black now that the news pages are white.

   1. THE CATEGORY FLAGS ON THE NEWS INDEX. `.news-cat` is the label in the top right of
      each card thumbnail, which is an overlay on an image on a white page, so it takes the
      inversion like every other one.
      SCOPED TO data-page="journal" ON PURPOSE. The identical cards appear on the HOME page
      inside .newsletter-block, and that block is still Kitto grey, so there the flags must
      stay yellow with black type. Same component, two grounds, two treatments: that is the
      rule working rather than an inconsistency.

   2. THE EXPANDED VIEWER'S CAPTION BAND. `.ph-viewer-cap` is the caption that sits OUTSIDE
      the picture, as a band across the foot of the screen, and Cam's note is that these go
      grey too. It was built to match the site footer, 12px ink on yellow. Grey with yellow
      type instead, so the caption reads the same whether it is on the picture or under it.
      The viewer's own ground stays yellow: that full-bleed field is the point of expanding,
      and it is what the grey band now reads against. */
body[data-page="journal"] .news-cat {
  background: var(--grey);
  color: var(--yellow);
}
body[data-page="journal"] .ph-viewer-cap,
body[data-page="journal"] .ph-viewer-cap p,
body[data-page="journal"] .ph-viewer-cap span,
body[data-page="journal"] .ph-viewer-cap .cap-sub {
  background: none;
  color: var(--yellow);
}
body[data-page="journal"] .ph-viewer-cap { background: var(--grey); }

/* ═══ AND THE EXPAND CONTROL (Cam, 26 Aug: "expand also needs to be grey and yellow text") ═══
   `.hero-expand` is the same yellow-block-with-black-type device as the captions, sitting in
   the bottom-left corner of a picture, so on the white news pages it inverts with them. The
   icon is stroked with currentColor, so it follows the type without a rule of its own.
   The home page hero keeps the yellow block: that ground is Kitto grey and the rule is about
   what is behind the picture, not about the control. */
body[data-page="journal"] .article-body .ph .hero-expand,
body[data-page="journal"] .media-full .ph .hero-expand {
  background: var(--grey);
  color: var(--yellow);
}

/* ═══ THE HIRE HEAD SPANS THE ROW ONCE THE GRID DROPS TO TWO (Cam, 26 Aug) ═══
   Screenshot 2026-08-26 19.00: on a tablet the "Hire" title and its standfirst sat visibly
   higher than the Projection photograph beside them.
   CAUSE: `.kit-head-card { align-self: start }`. At three columns the head is a small cell
   the cards flow around and starting it at the top is right. At two columns it becomes one
   half of a row whose height is set by a full product card, so a short block of centred text
   is pinned to the top of a cell several hundred pixels deep, with the photograph beside it
   optically centred much lower. Nothing is misaligned in the box model; it just reads as a
   mistake, which is the same thing.
   Spanning is the fix rather than `align-self: center`. Centring would only trade one odd
   pairing for another, a title floating in the middle of a column next to a picture. The
   head is the page title, the thing the grid belongs to, so below three columns it stops
   being a cell in the grid and goes back to being a heading above it.
   Bounded at the bottom by the single-column tier, where every item already spans and the
   rule would be doing nothing. */
@media (max-width: 992px) and (min-width: 641px) {
  .kit-head-card {
    grid-column: 1 / -1;
    align-self: auto;
    margin-bottom: clamp(.5rem, 2vw, 1.5rem);
  }
}

/* ═══ AND IT CENTRES IN ITS CELL ON DESKTOP (Cam, 10 Sep) ═══
   Screenshot 2026-09-10 12.41: "the text is a lot higher than the equipment and should be
   centred". The note above argued that at three columns `align-self: start` was right because
   the head is a small cell the cards flow around. It is not: the row is as tall as a product
   card, so a three line block pinned to the top of it sat level with the top of a projector
   photograph whose subject is optically halfway down. The title read as having slipped up out
   of its row.
   `center` here, `1 / -1` in the band below. They are two answers to the same complaint at two
   widths, and both are right for their own layout: at three columns the head is a cell and
   should behave like one; at two it is half a row and stops being a cell at all.
   MIN-WIDTH ONLY, so nothing here reaches the phone. The head's height on a phone was matched
   to the news pages a moment ago and this must not disturb it: 993 is the first width above
   the band that already handles the tablet, so the two cannot overlap. */
@media (min-width: 993px) {
  .kit-head-card { align-self: center; }
}

/* ═══ THE STROKE ROUND A PICTURE FOLLOWS THE SAME RULE (Cam, 26 Aug) ═══
   Same logic as the caption blocks: a yellow keyline drawn on a white page has nothing
   holding it there, and it was reading as a highlight rather than as an edge. On the news
   pages the picture's stroke goes grey. It stays yellow everywhere the ground is grey. */
body[data-page="journal"] .article-body .ph,
body[data-page="journal"] .media-full .ph,
body[data-page="journal"] .news-card .ph,
body[data-page="journal"] .news-card .news-card-media { outline-color: var(--grey); }

/* ═══ THE CATALOGUE FILTERS RATHER THAN RELOADS, AND FADES WHILE IT DOES (Cam, 26 Aug) ═══
   Rows the query has excluded fade out and then collapse; the ones that match stay put.
   The collapse is not optional: opacity alone would leave a column of gaps where the misses
   were, and the surviving rows would never close up into a readable list.
   THE `allow-discrete` IS WHAT MAKES IT FADE. The first version set `display: none` on the
   excluded rows and they simply vanished, because display is a discrete property and cannot
   be transitioned: the browser flips it on frame one and the opacity transition underneath
   never gets a chance to be seen. Naming display in the transition WITH allow-discrete
   changes when the flip happens: on the way out display holds at its old value for the whole
   .24s and switches to none at the end, so the fade plays first; on the way back in it
   switches immediately, so there is something on screen to fade up.
   @starting-style is the other half. A row returning from display:none has no previous style
   to transition FROM, so without this it would appear at full opacity and only the exit
   would animate. It gives that first frame an opacity of 0 to start from.
   The group heading goes with its rows: a category title standing over nothing reads as a
   list that failed to load rather than as a category with no match. */
/* ⚠️ THE SELECTOR HAS TO OUT-SPECIFY `body[data-page="equipment"] main *`, which sets a
   colour transition on literally every element in the page and, at (0,1,2), beat a plain
   `.catalogue li` at (0,1,1). The fade was written, computed, and silently discarded: the
   rows filtered correctly and simply snapped. Checked with getComputedStyle rather than by
   eye, which is the only way this shows up.
   The three colour properties are carried along deliberately. Overriding the shorthand drops
   whatever it was doing, and what it was doing here is the grey-to-white fade when the
   catalogue opens and closes. Take them out and the rows would jump colour while the rest of
   the page eased. */
.catalogue li, .catalogue .cat-group,
body[data-page="equipment"] main .catalogue li,
body[data-page="equipment"] main .catalogue .cat-group {
  opacity: 1;
  transition: opacity .24s var(--ease), display .24s allow-discrete,
              background-color .35s var(--ease), color .35s var(--ease), border-color .35s var(--ease);
}
@starting-style {
  .catalogue li, .catalogue .cat-group,
  body[data-page="equipment"] main .catalogue li,
  body[data-page="equipment"] main .catalogue .cat-group { opacity: 0; }
}
.catalogue li.is-filtered-out,
.catalogue .cat-group.is-filtered-out {
  opacity: 0;
  display: none;
}
/* A filter is not motion the way a moving element is, but honour the setting anyway: with
   it off the rows swap instantly, which is the honest version of the same behaviour. */
@media (prefers-reduced-motion: reduce) {
  .catalogue li, .catalogue .cat-group,
  body[data-page="equipment"] main .catalogue li,
  body[data-page="equipment"] main .catalogue .cat-group { transition: none; }
}

/* ═══ THE CARD TILES JOIN THE PAGE'S FADE (Cam, 26 Aug) ═══
   `.kit-card-media` sets its own white background so product photographs sit on white
   whatever the page is doing. That is right in every state except one: leaving the catalogue,
   where the page eases grey to white over .35s and the tiles, already white, stood out as a
   grid of bright rectangles on grey for the whole of it. Cam: "shows the square of the
   images, which I don't enjoy."
   Giving them the grey while the catalogue is open is the half of the fix that lives in CSS.
   The other half is in main.js, which now holds .catalogue-open for one frame after the grid
   is unhidden: without that the class is already gone by the time the tiles are painted and
   there is no grey for them to ease out of.
   The transition itself is inherited from `body[data-page="equipment"] main *`, so the tiles
   travel on exactly the same curve and duration as the page behind them. */
/* SUPERSEDED 10 Sep. This painted the card tiles' media grey while .catalogue-open was on,
   so they did not flash white against a page mid-transition. The page no longer changes
   colour at all, and these tiles sit behind a full-height panel while it is open, so there
   is nothing for it to cover. Left as a no-op rather than deleted only because the note it
   belongs to explains the flash it was written for. */
/* body.catalogue-open .kit-card-media { background: var(--grey); } */

/* ═══ THE WASH OVER A PICTURE FOLLOWS THE GROUND TOO (Cam, 26 Aug) ═══
   "The dithering fade / colour on the news and its expanded pages to grey rather than
   yellow... this again is for the images on white vs grey."
   Same rule as the caption blocks and the keylines, applied to the hover wash and the
   dither screen that sit ON the picture. A yellow wash is what the home page's grey ground
   wants; over a picture on white it reads as a highlighter rather than as the picture going
   coarse. The home hero keeps its yellow, because that ground is still grey.
   The expanded viewer's field goes with them: it is the same picture, one size up. */
body[data-page="journal"] .news-card .ph::after,
body[data-page="journal"] .news-card .news-card-media::after,
body[data-page="journal"] .article-body .ph::after,
body[data-page="journal"] .media-full .ph::after,
body[data-page="journal"] .article-body .ph::before,
body[data-page="journal"] .media-full .ph::before { background: var(--grey); }
body[data-page="journal"] .ph-viewer { background: var(--grey); }

/* ═══ [hidden] HAS TO OUT-SPECIFY THE LAYOUT (Cam, 26 Aug) ═══
   "Catalogue view on the monitors/displays doesn't do the catalogue view, it just makes the
   page grey."
   The [hidden] ATTRIBUTE only gets `display: none` from the browser's own stylesheet, and any
   author rule that sets display beats it. `.kit-grid { display: grid }` and
   `.chips { display: flex }` are exactly that, so setting `hidden` on them changed the
   attribute and nothing else: the cards and the filter chips stayed on screen, the catalogue
   was appended a full page below them, and all the reader saw was the page turning grey.
   The trap is that it LOOKS correct from script: `el.hidden` reads true, which is what I
   checked. Only the computed display tells the truth.
   Attribute selector, so it is (0,2,0) against the class rule's (0,1,0) and wins on
   specificity rather than on !important. */
.kit-grid[hidden], .chips[hidden] { display: none; }

/* ═══ A ONE-GROUP CATALOGUE IS CENTRED (Cam, 26 Aug) ═══
   `.catalogue` is a two-column block and `.cat-group { break-inside: avoid }` keeps each
   group whole so a category never splits across the fold. On the hire page there are seven
   groups and they distribute, which is what those two rules were written for.
   A CATEGORY page has one group, and one unsplittable block in a two-column layout goes in
   the first column and leaves the second empty, so Monitors came out as a tall list down the
   left half of the page with nothing beside it.
   First attempt flowed the ITEMS into two columns to fill the space. Cam's call is the other
   way: "just centre it if it's on its own, the width is fine for consistency." A row on a
   category page then measures exactly what a row on the hire page measures, which is the
   point, and the list sits in the middle of the page rather than hanging off one edge.
   The max-width IS one hire-page column: the container less one gutter, halved. Written from
   the same clamp the column-gap uses, so the two can never drift apart.
   :has rather than a class from main.js, so this also covers the hire page if a filter ever
   leaves one group standing, and any category page added later. */
.catalogue:has(> .cat-group:only-child) { column-count: 1; }
.catalogue > .cat-group:only-child {
  max-width: calc((100% - clamp(2rem, 5vw, 5rem)) / 2);
  margin-inline: auto;
}
/* Below the catalogue's own single-column breakpoint there is only ever one column, so the
   half-width cap would make the list half of an already narrow screen. Full width there. */
@media (max-width: 640px) {
  .catalogue > .cat-group:only-child { max-width: none; }
}

/* ═══ THE SEARCH ROW GOES TO THE FOOT OF THE SCREEN ON A PHONE (Cam, 26 Aug) ═══
   Even compacted to one line it was still ~60px of chrome stacked under a 72px nav, so the
   top of every hire page opened with 130px of furniture. Pinned to the bottom instead: the
   top of the page is the page again, and the two controls land where a thumb already is.
   It stays inside header.site, which is position: fixed at z-index 50, so it inherits that
   stacking context: above the basket panel at 46, below the mobile menu at 10000, which is
   the order those three want. header.site is pointer-events: none and this row already sets
   itself back to auto, so it stays clickable.
   TAKING IT OUT OF FLOW SHRINKS THE HEADER, WHICH IS THE POINT. --header-h is measured off
   #siteHeader by a ResizeObserver and every page's top padding is calculated from it, so the
   moment this row stops taking height up there, the whole page moves up with it. Nothing
   else has to be told.
   The border flips to the top edge: it is the line the bar sits ON, and the bar is now
   underneath the page rather than above it.
   The body's padding-bottom is the other half. Without it the last row of the catalogue, or
   the footer, sits behind the bar and cannot be scrolled clear of it. safe-area-inset keeps
   it off the home indicator on a modern iPhone. */
@media (max-width: 768px) {
  .nav-search-row {
    position: fixed; top: auto; bottom: 0; left: 0; right: 0;
    border-top: 2px solid var(--yellow);
    border-bottom: 0;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  body[data-page="equipment"] {
    padding-bottom: calc(62px + env(safe-area-inset-bottom, 0px));
  }
}

/* ═══ STANDFIRSTS GO GREY, AND THE ARTICLE SUBHEADS COME DOWN A STEP (Cam, 26 Aug) ═══
   The hire page's standfirst and the news standfirst are the same kind of line, a sentence
   under a title explaining what the page is, so they take the same colour.
   --mid-gray, NOT the brand grey. #89949b is 3.1:1 on white, under the 4.5 a paragraph of
   this size needs; --mid-gray is #7e7e7e and clears it. The brand grey is a FIELD colour on
   this site, the ground a picture or a panel sits on, and it has never been a text colour on
   white. See the contrast note at --grey. */
.kit-head-card .sub,
body[data-page="journal"] .page-head .sub { color: var(--mid-grey); }

/* Down one increment on the scale, --fs-25 (24px) to --fs-lead (22px). Cam: the subheads on
   the expanded news pages, the artist names like Kat Easto, are too big. The h1 is untouched:
   it was raised on purpose earlier today and it is the thing these sit under. */
body[data-page="journal"] .article-body h2 { font-size: var(--fs-lead); }
@media (max-width: 768px) {
  /* The phone rule set earlier today came down with it, same one step, so the two ends of
     the range stay in proportion rather than the mobile size overtaking the desktop one. */
  body[data-page="journal"] .article-body h2 { font-size: clamp(1.375rem, 5.8vw, 1.625rem); }
}

/* ═══ THE NEWS EXPANDED VIEW GOES FULL BLEED (Cam, 26 Aug) ═══
   "Expand on the news expanded pages needs to be full bleed the same way it does on the
   home page... full bleed, crop where due."
   .ph-viewer-stage was `object-fit: contain` inside a clamp of padding, so the picture sat
   in the middle of the window with a margin all round: bigger than the thumbnail, but still
   a picture ON a screen rather than filling it. Cover with no padding gives the window to
   the photograph and lets it crop, which is what expanding is for.
   max-width and max-height have to be cleared as well as the fit changed. They cap the box
   at the stage's size, and a capped box with cover inside it still letterboxes; the element
   has to be told to BE the stage before cover has anything to fill. */
body[data-page="journal"] .ph-viewer-stage { padding: 0; }
body[data-page="journal"] .ph-viewer-stage img {
  width: 100%; height: 100%;
  max-width: none; max-height: none;
  object-fit: cover;
}

/* ═══ AND THE DITHER, AS ON THE NEWS INDEX (Cam, 26 Aug) ═══
   The cards on the news index already do this: a 6px conic screen that fades up on hover so
   the picture reads as going coarse rather than as being tinted. The article pictures had
   the grey wash but not the screen, so the two behaved differently a click apart.
   Copied rather than shared because the index rule is bound to .news-card. Same 6px cell and
   the same neutral black-and-white values, which is why it needs no colour of its own: it
   darkens and lightens the picture underneath instead of laying a hue over it. */
body[data-page="journal"] .article-body .ph::before,
body[data-page="journal"] .media-full .ph::before {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    repeating-conic-gradient(rgba(0,0,0,.16) 0% 25%, rgba(255,255,255,.10) 0% 50%)
    0 0 / 6px 6px;
  opacity: 0; transition: opacity .25s var(--ease);
}
body[data-page="journal"] .article-body .ph:hover::before,
body[data-page="journal"] .media-full .ph:hover::before { opacity: 1; }

/* ═══ THE BASKET'S BUTTONS BELONG TO THE PANEL, NOT TO THE PAGE (Cam, 26 Aug) ═══
   "The checkout button is black and white on some views."
   Measured: on a .grey-page the Checkout button came out white on ink; on the hire page the
   same button is a yellow outline. The panel is identical in both, so the button was taking
   its colours from whatever page happened to be underneath it.
   CAUSE: `body[data-page="journal"] .button` and `body.grey-page .button` are page-level
   rules written for those pages' own grounds, and they are (0,2,0), exactly the same weight
   as `.basket-panel .button`. Source order decided, and the page rules come later, so they
   won inside a panel they know nothing about.
   The basket slides over EVERY page, and its ground is Kitto grey wherever it opens, so its
   controls have to be constant. Declared last so nothing page-specific can reach in again,
   and matching the treatment the hire page was already showing, which is the one that was
   right. The ghost variant already reads this way, so the pair now differ only in weight
   rather than in colour. */
/* ⚠️ THE DOUBLED CLASS IS LOAD-BEARING. `.basket-panel .button` is (0,2,0) and
   `body[data-page="journal"] .button` is (0,2,1), an attribute plus a class over one
   element, so the page rule wins on specificity no matter how late this is declared. The
   first version of this fix changed nothing for exactly that reason. Repeating the class
   takes it to (0,3,0), which clears the page rules without reaching for !important. */
.basket-panel.basket-panel .button,
.basket-panel.basket-panel .button.ghost,
.checkout-panel.checkout-panel .button,
.checkout-panel.checkout-panel .button.ghost {
  background: transparent; color: var(--yellow); border-color: var(--yellow);
}
.basket-panel.basket-panel .button:hover, .basket-panel.basket-panel .button:focus-visible,
.basket-panel.basket-panel .button.ghost:hover,
.checkout-panel.checkout-panel .button:hover, .checkout-panel.checkout-panel .button:focus-visible,
.checkout-panel.checkout-panel .button.ghost:hover {
  background: var(--yellow); color: var(--ink); border-color: var(--yellow);
}

/* ═══ THE HOVER EFFECT, IDENTICAL ACROSS ALL OF NEWS (Cam, 26 Aug) ═══
   "We need the exact same effect that happens on the news page, all across news, for some
   reason they look different."
   They looked different because on the article pages half the effect did not exist. The
   index cards build BOTH layers on .news-card .ph: a `::before` carrying the 6px dither and
   an `::after` carrying the wash, each with `content: ""`. The article pictures only ever had
   the dither. My earlier pass set a grey BACKGROUND on `.article-body .ph::after` and
   stopped there, and a pseudo-element without `content` generates no box at all, so that
   declaration painted precisely nothing. Confirmed by reading it back: content computed as
   `none`.
   Declared here in full, the same two layers in the same order at the same values as the
   index, so hovering a picture behaves the same wherever you meet it in news. The wash is
   grey rather than yellow because these sit on white, which is the standing rule. */
body[data-page="journal"] .article-body .ph::after,
body[data-page="journal"] .media-full .ph::after {
  content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: var(--grey);
  opacity: 0; transition: opacity .25s var(--ease);
}
body[data-page="journal"] .article-body .ph:hover::after,
body[data-page="journal"] .media-full .ph:hover::after { opacity: .5; }

/* ═══ THE HERO PICTURES FADE IN (Cam, 26 Aug) ═══
   The home page's arrival was the yellow overlay lifting to reveal a hero that was already
   fully there. Everything else on the site enters: the journal cards have .reveal, the
   statement has its own delayed pageIn. The pictures, which are the first and most important
   thing on the page, simply appeared.
   They now come up in sequence, and the sequence is the reason for the delays. .55s is the
   wait: the overlay takes 1.15s to clear on this page, so the tiles start while it is still
   thinning and arrive as it finishes, rather than starting underneath an opaque yellow field
   where nobody can see them. That is the same mistake the note at .statement-band h1 records,
   so the two now use the same reasoning and land a beat apart.
   The lead picture goes first and the other five follow at 80ms intervals, in reading order
   down the columns rather than in DOM order, so the eye is led through the arrangement.
   `both` holds each tile at opacity 0 through its own delay, which is what makes the stagger
   a stagger rather than six things fading at once.
   Explicit tile classes rather than :nth-child: the columns split the tiles into two parents,
   so nth-child would restart the count halfway through and two tiles would share a delay. */
@keyframes heroTileIn {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}
body.home .hero-tile { animation: heroTileIn .8s var(--ease) both; }
body.home .hero-feature    { animation-delay: .55s; }
body.home .hero-tile.tile-a { animation-delay: .63s; }
body.home .hero-tile.tile-c { animation-delay: .71s; }
body.home .hero-tile.tile-b { animation-delay: .79s; }
body.home .hero-tile.tile-d { animation-delay: .87s; }
body.home .hero-tile.tile-e { animation-delay: .95s; }
/* An arrival is exactly the kind of motion this setting is asking to be spared, and the
   pictures must never be left at the animation's start state if it cannot run. */
@media (prefers-reduced-motion: reduce) {
  body.home .hero-tile { animation: none; opacity: 1; transform: none; }
}

/* ═══ THE EXPANDED NEWS VIEW TAKES THE HOME HERO'S OVERLAY (Cam, 26 Aug) ═══
   Corrected after reading the home stage in the browser instead of trusting the comments.
   The note at .slider-cap says the site settled on "black on yellow everywhere", and I built
   this to it. The live home stage does not do that: its caption is a transparent, centred,
   FULL-WIDTH line of YELLOW type with no block, no stroke and no shadow, and the way out is
   the word "Close" in the same yellow sitting UNDERNEATH it, centred, also with no block.
   Measured: caption transparent at 1200x25, colour rgb(255,237,20); close transparent, same
   colour, centred, 38px below. That comment is stale and this follows the screen.
   No left/right buttons either, on Cam's instruction: one picture, filling the window, and a
   way out. The nav is hidden rather than removed from main.js so the same viewer keeps
   working anywhere else it is used. */
body[data-page="journal"] .ph-viewer-cap {
  left: 0; right: 0; transform: none;
  bottom: clamp(3rem, 7vw, 4.75rem);
  width: auto; min-height: 0; max-width: none;
  background: none;
  padding-inline: var(--gut);
  display: block; text-align: center;
}
body[data-page="journal"] .ph-viewer-cap p,
body[data-page="journal"] .ph-viewer-cap span {
  background: none; color: var(--yellow);
  padding: 0;
  -webkit-text-stroke: 0; text-shadow: none;
  font-size: var(--fs-13); line-height: var(--cap-lh);
}
/* Under the caption, centred, and it says what it does. The icon keeps its box but the word
   is what carries it, exactly as on the home stage. */
/* THE BORDER IS PART OF IT. Corrected against Cam's screenshot of the home stage: the way
   out is a 2px yellow-outlined button holding an X and the word Close, transparent inside,
   centred under the caption. I had it as bare text on the strength of a computed
   background-color of transparent, which is exactly what an outlined button reports. The
   fill was never the question; the stroke was, and I did not check it. */
body[data-page="journal"] .ph-viewer-close {
  top: auto; right: auto; left: 50%; transform: translateX(-50%);
  bottom: clamp(1rem, 2.5vw, 1.75rem);
  width: auto; height: auto;
  padding: 7px 14px;
  border: 2px solid var(--yellow);
  background: none; color: var(--yellow);
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font); font-size: var(--fs-13); line-height: var(--lh-flat);
  transition: background-color .2s var(--ease), color .2s var(--ease);
}
/* Fills on hover, the same way every other yellow-outlined control on the site does. */
body[data-page="journal"] .ph-viewer-close:hover,
body[data-page="journal"] .ph-viewer-close:focus-visible {
  background: var(--yellow); color: var(--ink); border-color: var(--yellow);
}
/* The work's title in italic, as on the home stage and as in print. The article pictures set
   theirs upright, so this has to be stated rather than left to the shared caption rule. */
body[data-page="journal"] .ph-viewer-cap p:not(.cap-sub) { font-style: italic; }
body[data-page="journal"] .ph-viewer-nav { display: none; }

/* ═══ THE NEWS MEASURE COMES IN (Cam, 26 Aug) ═══
   .article is 760px site-wide. At the 18px body that is around 90 characters a line, well
   past the 60 to 75 that reading research and every print convention settle on: the eye has
   to travel too far to find the start of the next line and long paragraphs start to slip.
   680px puts it near 80 and closer to 75 once the em dashes and spaces are counted, which is
   a real improvement without the column suddenly reading as a narrow web article. "A point
   that still makes sense", as Cam put it.
   Scoped to the news pages: .article is used elsewhere and this is a decision about reading
   long-form text, not about the class.
   The page-head takes it too, so the title, the date line and the body all keep the single
   left axis they were given earlier today. Narrowing only the body would have broken that. */
body[data-page="journal"] .article { max-width: 680px; }

/* ═══ THE ARTICLE STARTS SOONER (Cam, 26 Aug: "still too far away") ═══
   Two paddings sat between the title and the first paragraph: .page-head's own bottom, and
   `main > section { padding-block: var(--sec) }`, which is 87px at laptop width. Together
   about 117px of white on a page whose lead picture has been removed, so there was nothing
   in the gap at all.
   Both come down. --sec is the right rhythm BETWEEN sections further down the article; it is
   the wrong opening between a title and the text it introduces, which belong together. */
body[data-page="journal"] .page-head { padding-bottom: clamp(8px, 1vw, 14px); }
body[data-page="journal"] main > section { padding-top: clamp(.75rem, 1.5vw, 1.25rem); }
/* HIRE OPENS AT THE SAME HEIGHT AS NEWS (Cam, 10 Sep, comparing IMG_3358 and IMG_3359:
   "hire header text needs to match news height").
   Both pages put their head IN the grid as a card, .journal-head-card and .kit-head-card, and
   the note above brought the news one up. The hire page never got the same treatment: its
   first section is unclassed, so it kept the full `main > section { padding-block: var(--sec) }`
   and the title sat about 90px lower than the identical arrangement on News.
   Scoped OFF the catalogue, which sets its own smaller opening a few lines up and would
   otherwise be overridden by this depending on source order. */
body[data-page="equipment"]:not(.catalogue-open) main > section { padding-top: clamp(.75rem, 1.5vw, 1.25rem); }

/* ═══ AND THE PHONE TITLE COMES DOWN ONE STEP (Cam, 26 Aug) ═══
   Raised to clamp(2rem, 8.5vw, 2.75rem) earlier today when the news pages were still grey
   and the title was carrying a masthead band. On white, with the standfirst grey and the
   measure narrowed, it no longer has to do that work. One increment down the scale at every
   term, so it is the same setting smaller rather than a different one: about 29px on a 390px
   screen against the 33px it was, still comfortably above the 22px subheads under it. */
@media (max-width: 768px) {
  body[data-page="journal"] .page-head h1,
  body[data-page="journal"] .article h1 { font-size: clamp(1.75rem, 7.4vw, 2.375rem); }
}

/* ═══ THE COOKIE BANNER GOES FULL WIDTH AND SHORTER ON A PHONE (Cam, 26 Aug) ═══
   It was a 420px card floating clear of two edges, which is a desktop shape: on a phone the
   inset just wasted the width it was short of, and the card still stood 200px tall over the
   page. Edge to edge along the foot instead, so the width is free, and the height comes down
   by taking it out of the three places it was actually hiding.
   MOST OF IT WAS PADDING, not type. 1.75rem all round is 56px of vertical air on a box whose
   content is three short lines. 1rem gives 24px of that back on its own.
   The title carried the rest. 24px is a heading size for a heading; "We value your privacy"
   is a label on a consent notice, not a section head, and at 15px it still reads first
   without setting the depth of the whole box. Cam's call.
   safe-area-inset keeps the buttons off the home indicator now that the box touches the
   bottom edge. */
@media (max-width: 768px) {
  .cookie-consent {
    left: 0; right: 0; bottom: 0;
    width: auto;
    padding: 1rem clamp(1rem, 4vw, 1.5rem) calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  .cookie-title { font-size: var(--fs-15); margin-bottom: .35rem; }
  .cookie-text { margin-bottom: .85rem; line-height: var(--lh-body); }
}

/* ═══ PAGE HEADS SIT CLOSER TO THE BAR, EVERYWHERE (Cam, 26 Aug) ═══
   Measured on About at 1200px: 116px from the bottom of the fixed bar to the first line of
   type. 72px of that is the bar's own height and cannot move. The other 84px was two
   paddings that could: main's clearance carried +24px beyond the bar, and .page-head added
   another 60px on top of that.
   Both come down. The clearance keeps 8px, which is enough to stop type touching the bar's
   yellow rule, and the head's own opening halves. About lands near 38px instead of 84px.
   "In all instances", so this is the shared rule rather than a per-page override: About, the
   kit category pages, contact and the legal pages all read from it. The news pages are the
   exception and already have their own, set earlier today, because their head carries the
   bar clearance itself rather than taking it from main. Those selectors are more specific,
   so they keep winning and nothing here reaches them. */
.page-head { padding-block: clamp(20px, 2.5vw, 32px) 0; }
/* ⚠️ :not([data-page="journal"]) IS LOAD-BEARING, not tidiness. `body.grey-page main` and
   `body[data-page="journal"] main` are BOTH (0,1,2), one class or one attribute over two
   elements, so they tie and source order decides. Written plainly at the end of the file
   this rule won, and it overrode `body[data-page="journal"] main { padding-top: 0 }` — the
   rule that exists precisely because on news the bar clearance lives on .page-head instead.
   News pages then had the clearance TWICE and went to 103px from the bar while every other
   page came down to 38. Excluding them settles it on specificity rather than on order. */
body.grey-page:not([data-page="journal"]) main { padding-top: calc(max(72px, var(--header-h, 72px)) + 8px); }

/* ─── thank-you.html ──────────────────────────────────────────────────────────
   The end of the enquiry, so the page is deliberately short: the sentence, and the two
   ways onward.
   SET AT --fs-lead, NOT --fs-display. The first cut copied .about-contact, whose links sit
   at display size because there they ARE the content. Here they are not: --fs-display
   (36 to 48px) is larger than .page-head h1 at --fs-banner (27 to 36px), so "Back to the
   kit" came out bigger than "Thank you" and the page read as a links page with a caption.
   --fs-lead keeps them clearly onward links, secondary to the heading and the sentence. */
/* THE GREY BLOCK IS EVEN TOP AND BOTTOM (Cam, 10 Sep: "there is too much room underneath
   back to kit, I think we need to aim for the same amount of room from top to bottom of that
   grey block").
   It was 40px above the heading against 96 below the button, which is why it sat high in its
   own field. The 40 is two things added: the 8px of clearance body.grey-page puts on main
   past the fixed bar, and .page-head's own 32px top padding. The bottom is written as that
   same sum rather than as the 40px it currently resolves to, so if either half is retuned the
   block stays even instead of quietly going lopsided again.
   The space between the sentence and the button, the first value, is untouched: that is the
   gap inside the content, not the room around it. */
.thanks-onward { padding-block: clamp(1.5rem, 4vw, 2.5rem) calc(8px + clamp(20px, 2.5vw, 32px)); }
.thanks-onward p { font-size: var(--fs-lead); line-height: 1.3; }
.thanks-onward p + p { margin-top: .4em; }
/* CENTRED (Cam, 10 Sep). The head takes the shared centring rule alongside equipment and
   journal; the onward links are this page's own section, so they take it here. Nothing
   carries a max-width down here, so text-align alone does it and there is no block to
   centre with margin-inline as there is on h1 and .sub. */
body[data-page="thanks"] .thanks-onward .container { text-align: center; }
/* BACK TO THE KIT IS THE HOME PAGE'S "View equipment", IN YELLOW (Cam, 10 Sep).
   Same classes as that link, `button xs ghost auto`, so the size, padding and weight come
   from the shared control rather than being restated here. Only the colour changes, and it
   takes the yellow-ghost treatment already used by the basket and checkout panels:
   transparent with a yellow rule, filling yellow with ink text on hover.
   Scoped to this page's section rather than added to .button.ghost, because ghost buttons
   on every other page sit on white and must stay ink. */
body[data-page="thanks"] .thanks-onward .button,
body[data-page="thanks"] .thanks-onward .button.ghost {
  background: transparent; color: var(--yellow); border-color: var(--yellow);
}
body[data-page="thanks"] .thanks-onward .button:hover,
body[data-page="thanks"] .thanks-onward .button:focus-visible,
body[data-page="thanks"] .thanks-onward .button.ghost:hover {
  background: var(--yellow); color: var(--ink); border-color: var(--yellow);
}

/* ─── Back to top, on the foot of the last picture (Cam, 10 Sep) ──────────────
   "on the footer bottom right i'd also like a back to top button, in line with the image
   caption just right aligned." Then: "no button just text, italics on hover."
   So it is set as a caption, not as a control: same colour, same size, same line-height as
   the credit it sits beside, with the <button> element stripped of every default a button
   brings. It stays a <button> in the markup because it performs an action and there is no
   #top to link to, but nothing about it should read as a box.
   BOTTOM MATCHES `.slider-block .slider-cap`, NOT the base `.slider-cap`. The base sits at
   clamp(1.25rem, 3.5vw, 3rem) and the footer carousel overrides it to
   clamp(1rem, 2.5vw, 2rem), so the base value put this 16px above the caption at laptop
   width and the two read as two separate lines. If that override is retuned, this moves.
   ONE CAVEAT, LEFT AS DECIDED. Plain yellow type carries no ground of its own, so on a slide
   whose bottom right is bright, the current Sophie Clements slide is a white gallery wall,
   it goes faint. The captions have exactly the same exposure and are accepted as they are,
   and matching them was the instruction, so this matches them. */
.slider-top {
  position: absolute; z-index: 3;
  right: var(--gut);
  bottom: clamp(1rem, 2.5vw, 2rem);
  appearance: none; -webkit-appearance: none;
  background: transparent; border: 0; border-radius: 0;
  padding: 0; margin: 0; cursor: pointer;
  font-family: var(--font); font-size: var(--fs-13); line-height: var(--cap-lh);
  color: var(--yellow);
  font-style: normal;
  transition: font-style 0s;
}
/* Italic on hover, which is the same move the slide titles make: the work's title is set
   italic "as it would be set in print" further up. focus-visible gets it too, so the
   keyboard sees what the mouse sees. */
.slider-top:hover, .slider-top:focus-visible { font-style: italic; }
/* The caption is a centred inline run and this is pinned right, so on a narrow screen the
   two meet in the middle before anything else gives. It drops below the picture rather than
   across it: the credit is the thing that has to stay readable. */
@media (max-width: 700px) {
  .slider-top { position: static; display: block; margin: .75rem auto 0; width: max-content; }
}

/* ─── THE CATALOGUE DROPS DOWN AS A CARD (Cam, 10 Sep) ────────────────────────
   "I just don't want to be pulled upwards to see the list. It's almost as if it should just
   fall down from the top and is a card in itself."
   It used to be a SECTION IN THE FLOW that replaced the card grid, so opening it from
   halfway down a long hire page put a 300-item list around wherever you happened to be
   standing, and reaching "Media players" meant scrolling back up. Closing then landed you
   somewhere else again, which is what `preserveScroll` was written to soften.
   Now it is the same mechanism as .basket-panel, deliberately: fixed, off the top on a
   transform, sliding to zero, with its own scroll. The page underneath never moves, so
   closing puts the reader back exactly where they were with nothing to preserve.
   TOP IS --header-h, NOT 0. The basket covers the bar because it is a different context;
   this is a view OF the hire page, and the search field in that bar filters it live, so the
   bar has to stay reachable while the catalogue is open.
   FULL HEIGHT, not the basket's 82vh. It covers the grid completely, which is what keeps
   `body.catalogue-open`'s existing grey-and-yellow rules correct: they were written to
   recolour the whole page for the in-flow swap, and with the page hidden behind a full-height
   panel they now only ever show through this. */
.catalogue-panel {
  position: fixed; left: 0; right: 0;
  /* TOP: 0, NOT --header-h (Cam, 10 Sep: "when I scroll up past it on desktop I start to see
     elements underneath, that should not happen").
     Starting the panel below the bar left a seam exactly one header tall, and anything that
     changed that height, the search row's drop animation, a resize before the ResizeObserver
     has republished --header-h, a rubber-band overscroll at the top of the panel's own
     scroller, showed the page through it.
     It covers the viewport outright instead, and the CONTENT is padded clear of the bar
     rather than the box being moved down. header.site is z-index 50 against this panel's 44,
     so the bar still floats on top and the search field stays usable, which is the whole
     reason the panel was pinned below it in the first place. Same arrangement .basket-panel
     already uses: top: 0, and the bar over it. */
  top: 0;
  /* THE WHOLE SCREEN, DESKTOP AND PHONE BOTH (Cam, 10 Sep: "in both instances it should take
     the whole screen"). It was briefly capped at 88svh so its bottom stroke would show, which
     made it a banner with the hire page visible underneath. Cam's call is the other way: it
     runs to the foot of the viewport, and the 2px yellow rule is the last two pixels of the
     screen rather than an edge floating above the page.
     svh, not vh: on a phone vh is the tallest the viewport ever gets, so with the address bar
     showing the panel would run under it and the bottom of the list would be unreachable. */
  bottom: 0;
  z-index: 44;                     /* under the basket at 46, over everything else */
  background: var(--grey);
  /* A GREY BANNER WITH A YELLOW STROKE (Cam, 10 Sep). The bottom edge is the one that reads,
     since the other three run off the screen, and it is the same 2px yellow the search row
     above it uses, so the panel lands as a third register of the bar rather than as a
     separate object. */
  border-bottom: 2px solid var(--yellow);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;    /* a flick at the end of the list must not scroll the page */
  transform: translateY(-100%);
  transition: transform .42s var(--ease);
}
/* THE DROP AND THE SLIDE BACK (Cam, 10 Sep: "I want the animation drop and revert back in",
   "the grey just appears but I want to treat it like the basket").
   The transition above was being LOST. `body[data-page="equipment"] main *` is (0,1,2) and
   declares `transition: background-color, color, border-color`, which beat `.catalogue-panel`
   at (0,1,0) and replaced the transform transition entirely, so the banner had nothing to
   animate and simply appeared and vanished. Repeating the class takes this to (0,2,0), which
   clears it, and the panel finally behaves the way .basket-panel does: it can afford a plain
   `transition: transform .42s` because nothing on the pages it lives on sets a competing one.
   Written as its own rule rather than folded into the block above so the reason survives. */
.catalogue-panel.catalogue-panel { transition: transform .42s var(--ease); }
.catalogue-panel.open { transform: translateY(0); }
.catalogue-panel[hidden] { display: none; }
/* THE PANEL CARRIES THE PAGE'S MEASURE NOW. In the flow the catalogue sat inside a
   .container and inherited `main > section { padding-block: var(--sec) }`; as its own
   scroller it has neither, so the list ran flush to both edges of the screen and the first
   heading sat against the header. Same --gut and --max as .container, so the columns line up
   with the card grid the panel is covering rather than being a measure of their own. */
/* :not(.page-head), because the cloned head brings its OWN .container, which already carries
   the measure and the gutter. Given both, the head would sit one full gutter inside the list
   beside it. */
.catalogue-panel > *:not(.page-head) {
  width: 100%; max-width: var(--max); margin-inline: auto;
  padding-inline: var(--gut);
}
/* THE BLOCK PADDING GOES ON THE SCROLLER, NOT ON EACH CHILD. On the hire page #kitCatalogue
   holds a heading AND the list as separate children, so a padding-block on `> *` was applied
   to both and opened a gap between the two the size of two paddings. */
/* The top padding carries the header's height now that the box starts at 0, so the first
   thing in the panel still clears the bar. Read from --header-h, which main.js keeps in step
   with the real bar, so the search row's own height is already in it. */
.catalogue-panel { padding-block: calc(var(--header-h, 72px) + clamp(1.5rem, 3vw, 2.5rem)) clamp(3rem, 8vw, 6rem); }
@media (prefers-reduced-motion: reduce) {
  .catalogue-panel.catalogue-panel { transition: none; }
}
/* THE PAGE BEHIND DOES NOT SCROLL. Without this a flick that starts on the panel and runs
   past its end keeps going on the document underneath, so closing the catalogue returns the
   reader somewhere they never chose to be, which is the whole complaint this change exists
   to answer. overscroll-behavior above stops the chaining; this stops the page being
   scrollable at all while the panel owns the screen. */
html.catalogue-locked, body.catalogue-locked {
  overflow: hidden;
  /* OVERSCROLL TOO, NOT JUST OVERFLOW (Cam, 10 Sep: "when I scroll down past the list I see
     the page underneath, this should not happen and should just be grey").
     `overflow: hidden` stops the document SCROLLING, and that was already true; what it does
     not stop is the rubber-band BOUNCE. On macOS that bounce translates the whole viewport,
     fixed elements included, so a flick past the end of the list dragged the panel itself up
     and showed the hire page in the gap it left. The panel's own overscroll-behavior: contain
     only stops the scroll chaining to the document; it does not stop the document bouncing
     when the gesture never reached the panel in the first place, which is exactly what
     happens on a category page whose list is shorter than the screen.
     `none` rather than `contain`: contain still allows the element's own bounce, and here the
     element is the page. */
  overscroll-behavior: none;
}

/* The cloned head is a real .page-head, so it arrives with the page's own type hierarchy,
   measure and centring and needs nothing restated here. Only the space beneath it, since in
   the page that gap comes from the next `main > section` and inside the panel there is no
   section to follow it. Its own top padding is dropped: the panel already pads down past the
   header, and .page-head's would add a second opening on top of it. */
.catalogue-panel > .page-head { padding-block: 0 clamp(1.25rem, 2.5vw, 2rem); }

/* ─── ON A PHONE IT COMES UP, NOT DOWN (Cam, 10 Sep: "for mobile it should load up") ──────
   Below 768 the search row is `position: fixed; bottom: 0`, a bottom bar, so the control that
   opens the catalogue is at the FOOT of the screen. A panel dropping from the top would then
   animate away from the thumb that pressed it. It rises from the bottom edge instead, which
   is the same relationship the desktop panel has with the bar at the top: it arrives from the
   side its own control is on.
   The stroke moves with it. On desktop the yellow rule is the panel's bottom edge; here the
   panel meets the bar along its top, so that is the edge that carries the line, exactly as
   .nav-search-row itself flips its border at this width.
   The bottom padding clears the bar. It is 62px plus the safe-area inset, the same figure
   body[data-page="equipment"] uses at this width for the same reason, or the last rows of the
   list sit behind the bar and cannot be scrolled clear of it.
   The top padding drops to the nav's height alone: --header-h shrinks here, because taking
   the row out of flow is what makes the header short, and the panel reads that token. */
@media (max-width: 768px) {
  .catalogue-panel { transform: translateY(100%); }
  .catalogue-panel.open { transform: translateY(0); }
  .catalogue-panel {
    border-top: 2px solid var(--yellow);
    border-bottom: 0;
    padding-block: calc(var(--header-h, 72px) + clamp(1.5rem, 3vw, 2.5rem))
                   calc(62px + env(safe-area-inset-bottom, 0px) + clamp(1.5rem, 4vw, 2.5rem));
  }
}

/* ─── The legal pages' head, centred onto their own measure (Cam, 10 Sep) ─────
   rental-agreement.html carries data-page="" and cookies.html carries data-page="cookies",
   so neither is caught by the equipment/journal/thanks centring block up the file, and both
   opened with a left-aligned head over a centred body.
   Keyed off :has(.legal) rather than either of those values, so it follows the CONTENT: any
   page built with the legal layout is centred, and neither of those two attributes has to be
   remembered or corrected.
   A SEPARATE BLOCK, not another line in the equipment/journal list. One unsupported selector
   invalidates an entire selector list, and adding :has() to that one would take the working
   centring off equipment and journal in any browser without it. The same reason the hero's
   two :has and .hero-expanded rules are kept apart.
   h1 and .sub both carry a max-width, so they need margin-inline as well as text-align: the
   first centres the lines, the second centres the block they sit in. */
body:has(.legal) .page-head .container { text-align: center; }
body:has(.legal) .page-head h1,
body:has(.legal) .page-head .sub { margin-inline: auto; }

/* ─── Article pictures: the credit arrives on hover (Cam, 10 Sep) ─────────────
   "on news, captions are only on the hover."
   The same move .hero-tile-cap makes, and for the same reason: a picture in an article is
   the thing being read, and a yellow credit block sitting on it permanently is read first.
   On the prize piece that is three of them down one column, which is three labels between
   the reader and the work.
   Focus counts as hover, so tabbing surfaces what a pointer does. There is no link inside
   these figures, so :focus-within only fires if one is ever added; the rule costs nothing
   and means a future one behaves.
   ONLY WHERE HOVER IS A REAL INPUT, exactly as the hero block above states: on a touch
   screen there is no pointer to rest, so the credits stay visible rather than being hidden
   behind a gesture that does not exist. That matters more here than on the hero, because
   these credits carry the photographer's name and a phone is where most of this is read. */
@media (hover: hover) and (pointer: fine) {
  .article-body .ph .slider-cap,
  .media-full .ph .slider-cap {
    opacity: 0;
    transition: opacity .22s var(--ease);
  }
  .article-body .ph:hover .slider-cap,
  .article-body .ph:focus-within .slider-cap,
  .media-full .ph:hover .slider-cap,
  .media-full .ph:focus-within .slider-cap { opacity: 1; }
}

/* ─── Thank you fills the screen (Cam, 10 Sep) ────────────────────────────────
   "for mobile the page of course becomes mainly yellow because the footer needs to stretch,
   and we lose the image footer. Can we make sure that mobile is a full grey screen with
   yellow footer underneath, matching the viewport."
   This is the one page on the site with almost nothing on it, so on a phone, where the
   kit-at-work picture below the footer is hidden, the grey ran out after four lines and the
   rest of the screen was the yellow root showing through. A page that is a single sentence
   should look deliberate, not like it stopped loading.
   The body becomes a column and main takes the slack, which pushes the yellow band to the
   foot of the screen and lets the grey fill everything above it. min-height on the body
   rather than a height, so a narrow phone that DOES overflow still scrolls normally.
   svh, not vh: vh is the tallest the viewport ever gets on a phone, so with the address bar
   showing the footer would sit just off the bottom of the screen, which is the exact gap
   this exists to close.
   THE CONTENT CENTRES rather than sitting on the padding. The even top and bottom asked for
   earlier is what this preserves at full height: with main grown, equal padding would leave
   everything at the top with the space dumped below it. The padding stays as the minimum for
   when there is no slack to take. */
body[data-page="thanks"] { min-height: 100svh; display: flex; flex-direction: column; }
body[data-page="thanks"] main { flex: 1 0 auto; display: flex; flex-direction: column; justify-content: center; }

/* ═══ CHECKOUT FORM AS A SPREADSHEET GRID (Cam, 10 Sep) ═══════════════════════
   "what if we connected all the boxes together into a grid line structure, spreadsheet style,
   can we give that a go? we can revert if it looks silly."
   Eight separate outlined boxes floating in a panel became one ruled table. It suits what this
   actually is, a short form of flat values, and it suits the site: every other surface here is
   already built out of 2px yellow rules, so a grid is the existing language rather than a new
   one.
   HOW THE LINES ARE DRAWN, and this is the whole trick. The container takes the TOP and LEFT
   edges only, each cell takes its RIGHT and BOTTOM. Every rule is then drawn exactly once, by
   one element, so nothing doubles at a join and nothing doubles at the outer edge either.
   Giving the container all four and the cells all four is the obvious version and it produces
   4px on every outside edge and 4px between every pair.
   The fields lose their own boxes entirely and become the cell's contents: no border, no
   background, no height of their own. The cell is the box now. */
.checkout-form.checkout-form {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 0;
  border-top: 2px solid var(--yellow);
  border-left: 2px solid var(--yellow);
  margin: 1.4rem 0 2.4rem;
}
.checkout-form .form-item {
  /* One token for the cell's side padding, because the caret has to hang off the right rule
     by the same distance the label sits off the left one. Two numbers that must agree. */
  --co-cell-pad: .75rem;
  border-right: 2px solid var(--yellow);
  border-bottom: 2px solid var(--yellow);
  padding: .5rem var(--co-cell-pad) .6rem;
  min-width: 0;
}
/* The label is the column heading of its own cell, so it takes caption size and sits tight
   above the value rather than floating clear of it the way a form label does. */
.checkout-form .form-item label {
  font-size: var(--fs-13); line-height: var(--cap-lh);
  color: rgba(255,237,20,.72);
  margin-bottom: 1px;
}
/* ONE LEADING ACROSS EVERY CELL (Cam, 10 Sep: "leading needs to be consistent throughout all
   these elements, event address looks like it has more than anything else about project").
   It did. The inputs carried min-height: 1.9rem and an input CENTRES its text in whatever
   height it is given, so the value sat low in the cell; a textarea starts at the top of its
   box instead. Two different distances from label to value depending on the field type, which
   is exactly the sort of thing a ruled grid makes obvious.
   Every field is now one line box tall with the same line-height and no minimum of its own, so
   the value sits directly under its label in every cell. The MINIMUM MOVES TO THE CELL, where
   it belongs: it is there to stop a row collapsing to nothing, not to space the type inside
   it, and on the cell it applies equally whatever the field is. */
.checkout-form.checkout-form input,
.checkout-form.checkout-form select,
.checkout-form.checkout-form textarea {
  border: 0; background: transparent; color: var(--yellow);
  height: auto; min-height: 0; padding: 0; border-radius: 0;
  font-size: var(--fs-15); line-height: var(--lh-body); width: 100%;
}
.checkout-form.checkout-form textarea { min-height: calc(var(--lh-body) * 15px * 2); resize: vertical; display: block; }
.checkout-form .form-item { min-height: 3.6rem; }
/* THE FORM GRID KEEPS ITS BOTTOM RULE (corrected 10 Sep). It was removed here on a misreading:
   the line Cam wanted gone was the one BETWEEN the two grids, which belonged to the items
   list, not the one closing the form. Without it the table read as unfinished, which is what
   "there is a line missing underneath the top grid" is describing. */
/* THE CARET IS DRAWN ON THE CELL, NOT ON THE SELECT (Cam, 10 Sep: "drop down arrows are
   still not centred on the boxes student and technician required").
   It was painted on the select, and the select is now a single line box sitting UNDER its
   label, so `center` centred the caret on the value's line rather than on the cell. In a
   ruled grid the box is the cell, so that read as a caret pushed to the bottom of its box.
   Moving the background to .form-item makes `center` mean the middle of the cell, which is
   what it has to mean once the cell is the box. The select keeps appearance: none and the
   right-hand padding so its value still stops clear of the caret.

   A SEPARATE BLOCK for the :has() rule: one unsupported selector invalidates an entire
   selector list, so it cannot be joined to anything that has to keep working without it. */
.checkout-form.checkout-form select {
  appearance: none; cursor: pointer;
  padding-right: 1.3rem;
  background-image: none;
}
.checkout-form .form-item:has(select) {
  background-image: linear-gradient(45deg,transparent 50%,var(--yellow) 50%),linear-gradient(135deg,var(--yellow) 50%,transparent 50%);
  /* The caret's right edge lands on the cell's own padding, not 4px off the rule (Cam,
     10 Sep: "arrows are quite close to the edge"). Two 5px triangles, so the near one sits at
     the padding and the far one a triangle's width before it. */
  background-position: calc(100% - var(--co-cell-pad) - 5px) center, calc(100% - var(--co-cell-pad)) center;
  background-size: 5px 5px,5px 5px; background-repeat: no-repeat;
}
/* NO OUTLINE ON FOCUS (Cam, 10 Sep: "we can remove the boxes that happen when you click on
   them"). A ring drawn around the field is a box inside a cell that is already a box, which
   is the one thing this grid exists to stop.
   THE CELL LIGHTS INSTEAD, which is what a spreadsheet does: the active cell is shaded, not
   outlined. It also has to stay visible for anyone tabbing through, and removing the ring
   without replacing it would leave a keyboard user with no idea where they are.
   :focus-within on the cell rather than :focus on the field, so the whole cell responds and
   the tint sits behind the label as well as the value. */
.checkout-form.checkout-form input:focus,
.checkout-form.checkout-form select:focus,
.checkout-form.checkout-form textarea:focus { outline: none; }
/* background-COLOR, not the shorthand: the cell now carries the caret as a background-image
   and `background:` would blank it the moment the field took focus. */
.checkout-form .form-item:focus-within { background-color: rgba(255,237,20,.09); }
.checkout-form .form-item:focus-within label { color: var(--yellow); }
/* One column on a phone, and the rules still land once each: every cell is then a full row, so
   its right border is the table's right edge. */
@media (max-width: 768px) {
  .checkout-form.checkout-form { grid-template-columns: 1fr; }
}

/* ═══ THE ITEMS ARE A SECOND GRID, UNDER THE FORM (Cam, 10 Sep) ═══════════════
   "the items as they are added can be a separate grid underneath", and "the line underneath,
   I mean the break between the grid and the newsletter button".
   The list was ruled rows with a border-top on each, which under a ruled form read as the
   form's own grid carrying on and then stopping, and its last rule was the stray line sitting
   above the newsletter checkbox.
   Built the same way as the form so the two read as two tables rather than one that changes
   its mind: container takes the top and left, cells take their right and bottom, so every rule
   is drawn once. It gets its own top margin, which is what separates the two grids and makes
   the second one legible as a list of what is being asked for.
   THE LAST ROW HAS NO RULE UNDER IT, for the same reason as the form: nothing follows it but
   space, so the line was closing a box the eye had already read as open. That is the line Cam
   was pointing at. */
/* AN EMPTY LIST DRAWS NOTHING. The container carries the top and left rules, so with no rows
   in it they were a bare corner floating between the form and the newsletter, which is the
   doubled line that appeared the moment the basket was empty. :has() on a row, so the table
   only exists once there is something in it. */
/* THE HIDDEN ATTRIBUTE HAS TO WIN (Cam, 10 Sep: "checkout button should not appear on the
   basket if the basket is empty", "so in that case you can never view the checkout until you
   have something in your basket").
   main.js has set `qb.hidden = !hasItems` all along and it was doing nothing: .button declares
   `display: inline-flex`, which out-specifies the browser's own `[hidden] { display: none }`,
   so an empty basket still offered Checkout. This is the same fault .checkout-dates [hidden]
   was written for further up the file, and the same fix.
   Scoped to the basket foot rather than set globally, because a blanket [hidden] !important
   would silently break anything on the site that hides an element by class and shows it by
   attribute. */
.basket-foot [hidden] { display: none !important; }
.checkout-panel .checkout-items:not(:has(.checkout-item)) { border: 0; margin-top: 0; }
.checkout-panel .checkout-items {
  /* Two columns, not three: the remove control has gone, so the row is the name and the
     quantity being confirmed. */
  display: grid; grid-template-columns: 1fr auto;
  column-gap: 0; row-gap: 0;
  margin-top: 2.2rem;
  border-top: 2px solid var(--yellow);
  border-left: 2px solid var(--yellow);
}
.checkout-panel .checkout-item {
  display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
  align-items: center; padding: 0; border-top: 0;
  /* GAP ZERO, RESTATED (Cam, 10 Sep, screenshot: "lines broken here"). The phone rules for
     the old flex row are still above this and set `gap: .55rem .9rem`. Everything else in
     that block is overridden here, but gap was not, so the row held 14.4px between the name
     cell and the quantity cell: the cells draw their own rules, so that gap appeared as a
     notch where the horizontals stopped short of the vertical. In a ruled table the cells
     have to touch. */
  column-gap: 0; row-gap: 0;
  font-size: var(--fs-body);
}
/* The cells, not the row, carry the rules: a row cannot draw the verticals between its own
   children. Each direct child of the row is a cell. */
.checkout-panel .checkout-item > * {
  border-right: 2px solid var(--yellow);
  border-bottom: 2px solid var(--yellow);
  padding: .6rem .75rem;
  min-height: 3rem;
  display: flex; align-items: center;
}
/* THE LAST ROW KEEPS ITS RULE (corrected 10 Sep: "lines are not connecting on the bottom
   equipment grid"). Dropping it left the table open along the foot, so the two verticals ran
   down and stopped in mid air instead of meeting a line. The stray rule Cam wanted gone was
   this list's border when it was EMPTY, which is handled by the :has() rule above; a list with
   rows in it should close like the form does. */
/* The quantity column is a figure now, so it is centred in a cell wide enough for two digits
   and reads down the list like a spreadsheet column. */
.checkout-panel .checkout-item .co-qty { justify-content: center; min-width: 3.4rem; flex: 0 0 auto; }
/* THE NAME TRUNCATES, THE QUANTITY DOES NOT (Cam, 10 Sep: "equipment grid on checkout is
   broken, please fix, add ellipsis where due").
   .co-name is the cell and the cell is a flex box, so the name was an ANONYMOUS flex item and
   nothing could be set on it: text-overflow needs an element. main.js wraps the text in
   .co-name-t for exactly this, and the ellipsis goes there.
   min-width: 0 on both, or neither would shrink below its intrinsic width and the name would
   push the quantity column off the table rather than give ground. */
.checkout-panel .checkout-item .co-name { min-width: 0; gap: .35rem; }
.checkout-panel .checkout-item .co-name-t {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The category follows the name on the same line and is the first thing to go: it is the
   least of the two, so it shrinks first and the name keeps the room. */
.checkout-panel .checkout-item .co-type {
  display: block; margin-top: 0; flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.checkout-panel .checkout-item .co-type::before { content: ", "; }
/* The empty-basket row is one cell spanning the table, so it keeps a right border but has no
   neighbours to rule against. */
.checkout-panel .checkout-item .co-name:only-child { grid-column: 1 / -1; }
/* THE PHONE KEEPS THE GRID (Cam, 10 Sep, screenshot: names cut to "Sam...", "Abov...",
   "Adju..." with the quantity dropped onto a line of its own).
   This block used to send .checkout-items back to `display: block` for the phone, which left
   .checkout-item on `grid-template-columns: subgrid` with no grid parent to inherit from.
   Subgrid with nothing to subdivide falls back to a single implicit column, so the name and
   the quantity stacked and the name truncated against a column sized to nothing. That is the
   whole of the fault in the screenshot.
   The two-column table already works at any width, so it simply stays. Only the padding comes
   in, and the quantity column narrows, so the name keeps as much of the row as it can. */
@media (max-width: 576px) {
  .checkout-panel .checkout-items { margin-top: 1.6rem; }
  .checkout-panel .checkout-item > * { padding: .55rem .6rem; min-height: 2.6rem; }
  .checkout-panel .checkout-item .co-qty { min-width: 2.8rem; }
}

/* ═══ THE ENQUIRY FORM IS THE SAME RULED GRID (Cam, 10 Sep) ═══════════════════════════════
   "the very similar grid, minus the date adjustments, for the enquiries form", pointing at
   the Request a quote block: Name / Email / Mobile / Use type / Hire start date / Hire end
   date / Event address / About your project / Send enquiry.
   MINUS THE DATE ADJUSTMENTS means the checkout's date machinery does not come with it: no
   start-date-and-hire-length prompt, no "set exact dates" toggle above the table. The two
   date fields here are simply two more cells, because this form asks for a span rather than
   offering two ways to describe one.
   Built exactly as .checkout-form is, so the two forms on the site read as one thing: the
   container takes the top and left rules, the cells take their own right and bottom, and
   every line is therefore drawn once. The fields lose their boxes and become the contents of
   a cell, since a 2px box inside a 2px cell is the same edge stated twice.
   Scoped to `.form-block ...` rather than the bare class: the checkout markup carries BOTH
   .checkout-form and .form-inner but is never inside a .form-block, so nothing here can reach
   it and the two forms can be retuned separately.
   Yellow on grey in both places now, because they are the same kind of panel. */
.form-block .form-inner { display: block; }
.form-block .form-grid {
  display: grid; grid-template-columns: 1fr 1fr;
  column-gap: 0; row-gap: 0;
  /* stretch, NOT the `align-items: start` this rule used to carry. Start leaves each cell at
     its own content height, so the shorter of a pair stops before the row does and its
     bottom rule hangs in mid air. The cells have to fill the row for the table to close. */
  align-items: stretch;
  border-top: 2px solid var(--yellow);
  border-left: 2px solid var(--yellow);
}
.form-block .form-grid .form-item {
  --co-cell-pad: .75rem;
  border-right: 2px solid var(--yellow);
  border-bottom: 2px solid var(--yellow);
  padding: .5rem var(--co-cell-pad) .6rem;
  min-height: 3.6rem;
  min-width: 0;
}
/* The label is the column heading of its own cell: caption size, sitting tight above the
   value rather than floating clear of it the way a form label does. The global `label` rule
   carries !important on font-size, which is the same value, so only the margin needs saying. */
.form-block .form-grid .form-item label {
  font-size: var(--fs-13); line-height: var(--cap-lh);
  color: rgba(255,237,20,.72);
  margin-bottom: 1px;
}
/* One line box, one leading, no height of its own: the minimum belongs to the CELL, where it
   applies equally whatever the field is. An input centres its value in whatever height it is
   given and a textarea starts at the top of its box, so leaving the heights on the fields is
   what gives two cells in the same row two different distances from label to value. */
.form-block .form-grid input,
.form-block .form-grid select,
.form-block .form-grid textarea {
  border: 0; background: transparent; color: var(--yellow);
  height: auto; min-height: 0; padding: 0; border-radius: 0;
  font-size: var(--fs-15); line-height: var(--lh-body); width: 100%; max-width: 100%;
}
.form-block .form-grid input::placeholder,
.form-block .form-grid textarea::placeholder { color: rgba(255,237,20,.5); }
.form-block .form-grid select option { background: var(--grey); color: var(--yellow); }
/* The browser's own calendar glyph is black and a filter cannot reach a specific hue, so it
   is replaced outright with the same inline SVG the checkout's date fields use. */
.form-block .form-grid input[type="date"]::-webkit-calendar-picker-indicator {
  filter: none; opacity: 1; cursor: pointer;
  width: 15px; height: 15px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23FFED14' stroke-width='2' stroke-linecap='butt'%3E%3Crect x='3' y='5' width='18' height='16'/%3E%3Cpath d='M3 10h18M8 3v4M16 3v4'/%3E%3C/svg%3E");
  background-size: contain; background-repeat: no-repeat; background-position: center;
}
.form-block .form-grid textarea {
  min-height: calc(var(--lh-body) * 15px * 3); resize: vertical; display: block;
}
/* The caret is drawn on the CELL, not on the select. The select is a single line box sitting
   under its label now, so `center` on the select centres the caret on the value's line rather
   than on the box, and in a ruled grid the box is the cell. Same reasoning, and the same
   --co-cell-pad, as the checkout: the caret hangs off the right rule by exactly the distance
   the label sits off the left one.
   A SEPARATE BLOCK for the :has() rule, since one unsupported selector invalidates a whole
   selector list. */
.form-block .form-grid select {
  appearance: none; cursor: pointer;
  padding-right: 1.3rem; text-overflow: ellipsis;
  background-image: none;
}
.form-block .form-grid .form-item:has(select) {
  background-image: linear-gradient(45deg,transparent 50%,var(--yellow) 50%),linear-gradient(135deg,var(--yellow) 50%,transparent 50%);
  background-position: calc(100% - var(--co-cell-pad) - 5px) center, calc(100% - var(--co-cell-pad)) center;
  background-size: 5px 5px,5px 5px; background-repeat: no-repeat;
}
/* THE CELL LIGHTS INSTEAD OF THE FIELD BEING RINGED, which is what a spreadsheet does. A
   focus ring is a box drawn inside a box, the one thing this grid exists to stop, but it has
   to stay visible for anyone tabbing through. background-COLOR, not the shorthand, or it
   would blank the caret the cell now carries. */
.form-block .form-grid input:focus,
.form-block .form-grid select:focus,
.form-block .form-grid textarea:focus { outline: none; }
.form-block .form-grid .form-item:focus-within { background-color: rgba(255,237,20,.09); }
.form-block .form-grid .form-item:focus-within label { color: var(--yellow); }
/* THE BUTTON IS NOT A CELL, and it is not a row of the table either (Cam, 10 Sep: "please
   remove the line that goes down to Send enquiry"). The table's LEFT rule belongs to the
   container, so a button sitting inside that container as a borderless row still had the rule
   running down past the table's closed bottom edge and along beside it. main.js puts the grid
   in its own .form-grid now and the button is its sibling, which is the arrangement the
   checkout already has: the grid closes, then the action. */
.form-block .form-inner .submit-btn { margin-top: 1.6rem; }
/* One column on a phone, and the rules still land once each: every cell is then a full row,
   so its own right border is the table's right edge. This restates the width at which
   .form-inner already went single column, because the rules above out-specify that one. */
@media (max-width: 768px) {
  .form-block .form-grid { grid-template-columns: 1fr; }
}

/* Technician lead-time hint under the checkout's technician question (T225, 10 Sep 2026).
   Only shown for Yes or Unsure, see the toggle in main.js. Sized and coloured as the other
   small print in this panel (.checkout-note), so it reads as a note rather than an error. */
.co-hint {
  font-size: var(--fs-12); line-height: var(--lh-narrow);
  color: var(--mid-grey); margin: .4rem 0 0;
}

/* Netlify honeypot. Off-screen rather than display:none, because some bots skip anything
   that is not rendered and the field only works if they see it and fill it in. Kept out of
   the tab order and hidden from assistive tech on the field itself. */
.quote-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* Failure message on the quote form, shown in place rather than navigating away, so the
   customer keeps what they typed and can retry. Matches .checkout-error, which does the
   same job in the basket. */
/* Same treatment as .checkout-error, and for the same reason. This form sits inside
   .form-block.grey, so the dark red it first carried was 2.36:1 against the ground and
   was no more readable than the checkout's pale pink at 1.82:1. A yellow block with ink
   type is what the site already does to put something on the grey. */
.quote-error {
  font-size: var(--fs-15); line-height: var(--lh-narrow);
  color: var(--ink); background: var(--yellow);
  padding: .5rem .7rem; margin: .8rem 0 0;
}
