:root{--brand:#d4a574;--brand-ink:#111111;--brand-text:#8b5e2b;--ink:#1a1410;--ink-muted:#866650;--surface:#faf5f0;--surface-alt:#f6ede5;--radius:10px;--font:Georgia, Cambria, 'Times New Roman', Times, serif}
/*
  ── ONE MEASURE FOR A COLUMN OF CONTENT ─────────────────────────────────────────────────────────

  Reported with a photograph: on an article the hero photograph ran the full 70rem while the text
  under it stopped at 42rem, so a reader got a 1,120px image sitting on a 672px paragraph and 450px
  of empty page beside it. It looks like a mistake because it is one.

  THE RULE, AND IT IS THE RULE FOR EVERY SITE THIS RENDERS: anything that reads as a single column --
  running text, a heading above it, one photograph with it -- is this wide, and lines up on both
  edges. Only a band of SEVERAL things across (a card grid, a gallery, the Instagram strip) spans the
  whole wrap, because there the item width is what matters and the band reads as a band.

  56rem, AND THE TYPE GOES UP WITH IT. 42rem was the first answer and it was too small: on a 1,400px
  laptop it left a third of the page empty beside every paragraph, which the client reported as "the
  text/images width now is way too small". A column can only be widened so far before the eye loses
  its place coming back to the left margin, and the fix for that is not a narrower column but bigger
  type -- 56rem of 20px runs about 90 characters, which is where a wider column has to sit. Above the
  45-75 the textbooks give, and a deliberate choice rather than a drift: these are one-column pages on
  a big screen, and 79 characters in a third of the window reads as a mistake whatever the textbook
  says.
*/
:root{--measure:56rem}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;
  font-family:var(--font);
  /* 17px: these sites are read on a phone, often outdoors, often by someone over 40. */
  font-size:17px;
  line-height:1.6;
  color:var(--ink);
  background:var(--surface);
}
img{max-width:100%;height:auto;display:block}
/*
  NO SINGLE WORD MAY WIDEN THE PAGE. One long token -- a URL somebody pasted, an email address, a
  German compound, a phone number with no spaces -- is enough to make a 360px screen scroll sideways,
  and the symptom is the whole page shifting under a thumb rather than anything that looks like a bug
  in the word that caused it. break-word rather than anywhere: it only breaks a word that has nowhere
  else to go, so ordinary prose is untouched.
*/
body{overflow-wrap:break-word}
a{color:var(--brand-text);text-decoration-thickness:.08em;text-underline-offset:.15em}
h1,h2,h3{line-height:1.25;margin:0 0 .5em}
h1{font-size:2rem}
h2{font-size:1.5rem}
h3{font-size:1.2rem}
p,ul,ol{margin:0 0 1em}
:focus-visible{outline:3px solid var(--brand);outline-offset:2px}

/*
  THE HORIZONTAL GUTTER LIVES HERE AND NOWHERE ELSE.

  .wrap sets it. Three classes are put on the SAME element as .wrap -- hero, section and
  site-header__inner -- and all three used the padding shorthand with a vertical value and a zero:
  "padding:2.5rem 0". Same specificity, declared later, so each of them silently reset the inline
  padding to zero. Every wrapped row on every page of every site was flush against both edges of the
  screen. On a phone that is what it looks like: headings, body text and buttons all starting at x=0.

  Reported as "there is always a padding on all sides on mobile" alongside the broken menu, which is
  how it was found -- a photograph, then a measurement: getComputedStyle said 0px while the .wrap rule
  in the same stylesheet said 1.25rem.

  SO EVERY CO-CLASS USES padding-block, which cannot touch the inline axis however it is written.
  cssSource.test.ts derives the co-classes from the rendered HTML and fails if any of them sets a
  padding that reaches sideways -- the shorthand included.
*/
.wrap{width:100%;max-width:70rem;margin:0 auto;padding:0 1.25rem}
/*
  Running text, at the shared measure. See --measure.

  THE TYPE IS BIGGER HERE THAN ANYWHERE ELSE ON THE PAGE, and it is the same decision as the measure:
  a 56rem column at the 17px body size would be 105 characters a line. 1.2rem brings it back to about
  90. It is scoped to prose rather than set on the body so that cards, the nav and the footer are not
  dragged along with it -- a card is a label, not something anybody reads a paragraph of.
*/
.prose{max-width:var(--measure);font-size:1.2rem;line-height:1.65}
/* Headings keep their step above the body they sit in, now that the body has grown. */
.prose h2{font-size:1.75rem}
.prose h3{font-size:1.35rem}
/*
  LATEST ARTICLES. A list, not a card grid: three headlines with a line of blurb is what somebody
  scanning a home page reads, and a photograph for each would compete with the gallery above it.
*/
/* The Instagram strip. Three across on a laptop, two on a phone: a 1:1 tile below 150px is a
   thumbnail nobody can read, and three on a 360px screen is 104px each. */
.insta{list-style:none;padding:0;margin:0;display:grid;gap:.5rem;grid-template-columns:repeat(3,1fr)}
@media (max-width:36rem){.insta{grid-template-columns:repeat(2,1fr)}}
.insta__link{display:block;line-height:0}
/* SQUARE, ASSERTED HERE. Instagram's API returns no dimensions, so the aspect ratio is ours to set;
   without it a portrait post makes one row twice the height of the others. */
.insta__img{width:100%;aspect-ratio:1/1;object-fit:cover;border-radius:var(--radius);display:block}
.insta__caption{margin:.35rem 0 0;font-size:.85rem;color:var(--ink-muted)}
.insta__more{margin:1.25rem 0 0}
.booking__intro{max-width:var(--measure)}
/* The picker's own list, served from the API and styled by this same sheet. */
.slots{list-style:none;padding:0;margin:.5rem 0 1.5rem;display:flex;flex-wrap:wrap;gap:.5rem}
.slots__choice{display:grid;gap:.35rem;max-width:32rem}
.slot{display:block;padding:.35rem 0}
.booking .field{margin:.75rem 0}
/* The floating booking button.

   Fixed, with its bottom offset inside env(safe-area-inset-bottom) so it clears the iPhone home
   indicator -- without that the button sits under the bar and the tap opens the app switcher.

   RIGHT ON DESKTOP, FULL WIDTH ON A PHONE. A 56px pill in the corner of a 360px screen covers the
   text beside it; a bar across the bottom covers nothing and is the easier target one-handed. The
   body gets padding to match so the last line of every page is not underneath it. */
.book-cta{position:fixed;z-index:20;right:1rem;bottom:calc(1rem + env(safe-area-inset-bottom));
  display:inline-flex;align-items:center;justify-content:center;min-height:3.25rem;padding:0 1.25rem;
  border-radius:999px;background:var(--brand);color:var(--brand-ink);font-weight:700;
  text-decoration:none;box-shadow:0 6px 20px rgba(0,0,0,.28)}
.book-cta:hover{filter:brightness(1.06)}
body:has(.book-cta){padding-bottom:5.5rem}
@media (max-width:36rem){
  .book-cta{left:1rem;right:1rem;border-radius:var(--radius)}
}
/* A visitor who has said they do not want motion still gets the button; it simply does not lift. */
@media (prefers-reduced-motion:no-preference){
  .book-cta{transition:transform .18s ease, filter .18s ease}
  .book-cta:hover{transform:translateY(-2px)}
}
.section{padding-block:2.5rem}
.section--alt{background:var(--surface-alt)}
.muted{color:var(--ink-muted)}

/* Header and navigation. The only JavaScript on any page toggles this menu. */
.site-header{border-bottom:1px solid var(--surface-alt);position:sticky;top:0;background:var(--surface);z-index:10}
.site-header__inner{display:flex;align-items:center;justify-content:space-between;gap:1rem;padding-block:.75rem;flex-wrap:wrap}
/*
  ONE LINE ON A DESKTOP, and the wrap kept for the phone.

  With nine menu items the row wrapped and the menu dropped underneath the logo -- measured at 121px
  of header at 360px and 180px at 768px, a quarter of a tablet screen. Grouping the services takes it
  to six items, and nowrap here is what stops the row breaking again the moment a seventh arrives:
  the list shrinks its gaps instead.

  The wrap stays below 48rem because that is where the drawer lives, and the drawer IS a second row.
*/
@media (min-width:48rem){
  .site-header__inner{flex-wrap:nowrap}
  .nav{min-width:0}
  .nav__list{flex-wrap:nowrap;gap:clamp(.6rem,1.4vw,1.25rem)}
}
.site-header__name{font-weight:700;font-size:1.1rem;color:var(--ink);text-decoration:none;display:inline-flex;align-items:center;gap:.6rem;min-width:0}
/*
  HEIGHT-CONSTRAINED, WIDTH FREE. A trade logo is either a wide lockup (a name beside a mark) or a
  square badge, and the only rule that flatters both is "fit the header's height, take whatever width
  that needs". An explicit height with width:auto is what stops the intrinsic width attribute
  squashing a wide logo into a square, and max-width keeps a very wide one from pushing the nav off a
  phone screen.
*/
.site-header__logo{height:36px;width:auto;max-width:min(60vw,260px);object-fit:contain;display:block}
/*
  THE WORDMARK BESIDE THE LOGO.

  A client whose logo is a monogram had a header that did not say who they were, and there was no field
  for the name until theme.headerWordmark. The anchor is already a flex row, so the two sit side by side
  with a gap; the name inherits the page font rather than trying to imitate a logotype, which is the
  honest choice when the mark is doing the branding.

  min-width:0 with truncation, because a long trading name on a 390px phone would otherwise push the
  Menu button off the edge -- and a name that is clipped is better than a header that is broken.
*/
.site-header__wordmark{font-weight:600;font-size:1.05rem;line-height:1.2;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.nav__toggle{
  display:inline-flex;align-items:center;gap:.5rem;
  /* 44px minimum: this is tapped by someone holding a phone in one hand. */
  min-height:44px;padding:.5rem .75rem;
  background:none;border:1px solid var(--ink-muted);border-radius:var(--radius);
  font:inherit;color:var(--ink);cursor:pointer;
}
/*
  THE BARS. One span plus its two pseudo-elements, so the middle line needs no markup and the X it
  becomes when open is three transforms rather than a second icon.
*/
.nav__bars{position:relative;display:block;width:1.15rem;height:2px;background:currentColor;border-radius:2px;transition:background-color .15s ease}
.nav__bars::before,.nav__bars::after{content:'';position:absolute;left:0;width:100%;height:2px;background:currentColor;border-radius:2px;transition:transform .18s ease,top .18s ease}
.nav__bars::before{top:-6px}
.nav__bars::after{top:6px}
/* Open: the middle bar goes, the outer two cross. */
.site-header__inner[data-open=true] .nav__bars{background:transparent}
.site-header__inner[data-open=true] .nav__bars::before{top:0;transform:rotate(45deg)}
.site-header__inner[data-open=true] .nav__bars::after{top:0;transform:rotate(-45deg)}

.nav__list{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:1rem}
.nav__list a{color:var(--ink);text-decoration:none;display:inline-block;min-height:44px;line-height:44px}
.nav__list a[aria-current=page]{text-decoration:underline;text-decoration-color:var(--brand)}
/*
  THE GROUP YOU ARE INSIDE, underlined like the page you are on. It replaces a panel that used to
  render open on those pages -- see navItem -- and it is the same cue in the same colour, so a visitor
  on a service page can still see which menu it belongs to without a dropdown hanging over the page.
*/
.nav__group[data-current=true] > .nav__details > .nav__label{
  text-decoration:underline;text-decoration-color:var(--brand);text-underline-offset:.15em
}

/*
  ── THE SUBMENU ─────────────────────────────────────────────────────────────────────────────────

  A details/summary, and it needs no script. The first version opened on hover with the panel at
  display:none, and was keyboard-unreachable: an element inside a display:none subtree cannot be
  focused, so Tab never enters the panel, so :focus-within never fires. Found by driving a browser at
  it -- focusing the first child link left activeElement on the body. Every CSS-only way of properly
  hiding a panel removes focusability too, so the answer is a real control rather than a cleverer
  selector.

  It opens on CLICK at every width. Hover menus do not exist on the phones these sites are mostly
  read on, and one behaviour everywhere beats two that diverge.
*/
.nav__details{position:relative}
.nav__label{
  color:var(--ink);display:inline-flex;align-items:center;gap:.35rem;
  min-height:44px;cursor:pointer;list-style:none
}
/* The default triangle, off in both dialects, so the caret below is the only marker. */
.nav__label::-webkit-details-marker{display:none}
.nav__label::marker{content:''}
/* Drawn rather than an entity: no font support needed, and it inherits the colour. */
.nav__label::after{
  content:'';width:0;height:0;
  border-left:.28rem solid transparent;border-right:.28rem solid transparent;
  border-top:.32rem solid currentColor;opacity:.7
}
.nav__details[open] > .nav__label::after{transform:rotate(180deg)}
.nav__sub{list-style:none;margin:0;padding:0}

@media (min-width:48rem){
  /*
    ── A PANEL, AND NOTHING HERE DECIDES WHETHER IT SHOWS ────────────────────────────────────────

    The details element does that, and the nav script sets its open attribute on hover. This block
    only says what the panel LOOKS like once it is open.

    IT USED TO TRY, and that is worth writing down because it looks so reasonable: display:none here
    and display:block on .nav__group:hover. A current Chrome wraps details contents in a
    ::details-content pseudo-element with content-visibility:hidden while closed, and no display
    on the child un-skips a skipped subtree. The panel had a box, reported display:block and a
    192x173 rect, and was never painted -- elementFromPoint over its middle returned the hero
    section behind it. A client saw a menu that did not open at all.

    ABSOLUTE, so opening it does not push the header taller: a menu that changes the height of the
    page as you use it moves whatever you were reading.
  */
  .nav__sub{
    position:absolute;top:100%;left:-.75rem;z-index:30;min-width:12rem;
    padding:.35rem 0;
    background:var(--surface);border:1px solid var(--surface-alt);border-radius:var(--radius);
    box-shadow:0 .5rem 1.5rem rgba(0,0,0,.12)
  }
  .nav__sub a{display:block;padding:0 .9rem;white-space:nowrap;min-height:40px;line-height:40px}
  .nav__sub a:hover{background:var(--surface-alt)}
}

@media (max-width:47.99rem){
  /* In the drawer it is an indented list under its own row, which is what a drawer wants. */
  .nav__sub a{padding-left:1rem;opacity:.9}
  .nav__label{font-weight:600;width:100%}
}
/*
  ── THE MOBILE MENU ──────────────────────────────────────────────────────────────────────────────
  
  A DRAWER OVER THE PAGE, not a list that pushes it down. Asked for as a redesign: "ship a hamburger
  icon + overlay drawer for all clients."

  IT DEGRADES TO A PLAIN LIST WITH NO JAVASCRIPT, and that is the reason for data-js rather than
  styling the drawer unconditionally. The old arrangement claimed to work without scripting and did
  not: the list was display:none on a phone and only the script could reveal it, so a visitor with
  scripting off or a script that failed to load had NO navigation at all on a phone. The test that
  guarded it asserted the markup was present, which was true and not the point.

  So the base state here is the plain stacked list -- usable, plain, no button, which is the honest
  no-JavaScript answer -- and the script adds data-js to switch to the hamburger. A button that cannot
  work is never shown.
*/
@media (max-width:47.99rem){
  .nav{width:100%}
  .nav__toggle{display:none}
  .nav__list{flex-direction:column;gap:0;width:100%;padding-block:.25rem .5rem}
  .nav__list li+li{border-top:1px solid var(--surface-alt)}
  .nav__list a{display:block;width:100%}

  /* Scripting is available, so the hamburger can do something. */
  .site-header__inner[data-js=true] .nav{width:auto}
  /*
    ABOVE THE DRAWER, so the X is visible and tappable while it is open.

    The drawer is anchored to the top-right and covers the button that opened it -- so the first
    version could only be closed with Escape or the scrim, neither of which is discoverable, and the
    X the bars morph into was hidden underneath. A photograph of the open drawer showed it at once.
  */
  .site-header__inner[data-js=true] .nav__toggle{display:inline-flex;position:relative;z-index:50}
  .site-header__inner[data-js=true] .nav__list{
    /*
      Fixed to the right edge and the full height. inset-inline-start:auto rather than a width plus a
      left, so the drawer is anchored to the side it slides from and nothing has to agree about the
      arithmetic.
    */
    position:fixed;inset:0 0 0 auto;width:min(20rem,82vw);
    z-index:40;
    background:var(--surface);border-left:1px solid var(--surface-alt);
    box-shadow:-8px 0 24px rgb(0 0 0 / .14);
    /* Room for the header the drawer slides out from under, and a gutter of its own. */
    padding:5rem 1.25rem 1.5rem;
    overflow-y:auto;overscroll-behavior:contain;
    transform:translateX(100%);transition:transform .22s ease;
  }
  .site-header__inner[data-js=true][data-open=true] .nav__list{transform:none}
  /*
    THE SCRIM, as a pseudo-element of the header row. A real element would have to be in the markup of
    every page for a state that is off almost always; this costs nothing when shut.
  */
  .site-header__inner[data-js=true][data-open=true]::after{
    content:'';position:fixed;inset:0;z-index:30;background:rgb(0 0 0 / .45);
  }
  .site-header__inner[data-js=true] .nav__list li+li{border-top:1px solid var(--surface-alt)}
  .site-header__inner[data-js=true] .nav__list a{padding-block:.15rem;font-size:1.05rem}
}
/*
  The drawer is only ever a phone thing: above 48rem the nav is the ordinary horizontal list and the
  button is gone, so none of the fixed positioning above can apply.
*/
@media (min-width:48rem){.nav__toggle{display:none}}

/* Buttons. Big, solid, high contrast: the primary action is almost always a phone call. */
.btn{
  display:inline-flex;align-items:center;justify-content:center;
  min-height:52px;padding:.75rem 1.5rem;
  background:var(--brand);color:var(--brand-ink);
  border:0;border-radius:var(--radius);
  font:inherit;font-weight:600;text-decoration:none;cursor:pointer;
}
.btn--ghost{background:transparent;color:var(--brand-text);border:2px solid var(--brand-text)}

.hero{padding-block:3rem}
/*
  THE HERO IS A COLUMN, so it is held to the same measure as the text below it -- heading, standfirst
  and photograph all ending on the same right edge. Every page shape this renders leads with one:
  home, prices, about, the journal index and every article. It was the widest thing on all of them.
*/
.hero__heading,.hero__sub,.hero__media,.hero__slider{max-width:var(--measure)}
.hero__heading{margin-bottom:.5rem}
.hero__sub{font-size:1.15rem;color:var(--ink-muted);margin-bottom:1.5rem}
.hero__media{margin-top:1.5rem;border-radius:var(--radius);overflow:hidden}
/*
  THE HERO IMAGE FILLS ITS COLUMN, and the global img max-width rule is not enough to make it.

  That rule caps an image at its container and otherwise leaves it at its intrinsic width -- so a
  photograph narrower than the measure sits at its own size with a gap beside it, which on a hero reads
  as a broken layout rather than as a small picture. It became visible the moment heroes started
  falling back to the 640-wide card variant inside a 56rem column: reported as "the hero images are
  smaller than the container, so it looks off".

  Upscaling a 640-wide photograph to 896 is slightly soft and looks INTENTIONAL, which a gap does not.
  The global height:auto keeps the aspect ratio, so nothing is stretched.

  NO BACKTICKS IN THIS FILE. The stylesheet is one template literal and a backtick ends it; cssSource
  .test.ts guards it, and this comment was written with three of them on the first attempt.
*/
.hero__img{width:100%}

/*
  THE HERO SLIDER: a scroll-snap row the visitor drives.

  NO AUTO-ADVANCE. A hero that changes under somebody reading the heading is what every carousel is
  criticised for, and WCAG 2.2.2 wants a pause control for anything moving on its own beyond five
  seconds -- a control that then has to be found, styled and explained. A slider nobody has to stop
  needs none of that, and swiping costs no JavaScript because a phone already knows how to scroll.

  scroll-snap-align:center with one slide per frame is what makes a swipe land on a photograph rather
  than between two. scrollbar-width:none hides the bar the desktop platforms would otherwise draw
  across the bottom of the picture.

  The arrows are hidden until a pointer exists: on a touch screen they cover the photograph to offer
  something the finger already does better. any-pointer:fine is true for a mouse or a trackpad and
  false for a phone.
*/
.hero__slider{position:relative;margin-top:1.5rem}
.hero__slides{list-style:none;margin:0;padding:0;display:flex;gap:.5rem;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;-webkit-overflow-scrolling:touch;border-radius:var(--radius)}
.hero__slides::-webkit-scrollbar{display:none}
.hero__slide{flex:0 0 100%;scroll-snap-align:center;border-radius:var(--radius);overflow:hidden}
.hero__arrow{display:none}
@media (any-pointer:fine){
  .hero__arrow{position:absolute;top:50%;transform:translateY(-50%);border:0;cursor:pointer;background:rgba(0,0,0,.55);color:#fff;font-size:1.75rem;line-height:1;width:2.75rem;height:2.75rem;border-radius:999px;display:grid;place-items:center}
  .hero__arrow:hover{background:rgba(0,0,0,.8)}
  .hero__arrow--prev{left:.75rem}
  .hero__arrow--next{right:.75rem}
}

.grid{display:grid;gap:1.25rem}
/*
  THE GALLERY: SMALL SQUARE THUMBNAILS, AND THE BIG PICTURE ONLY WHEN ASKED FOR.

  It used to lay the full 1024px image out three to a row -- a megabyte a screen, and no way to see any
  one photograph properly. Now it is the 320px square thumb, and a tap opens the viewer below.

  A GRID AT EVERY WIDTH, and it was a swipeable strip on a phone until the agent described what that
  actually looked like: "on mobile the studio gallery renders as a single horizontal scrolling strip
  showing about two and a half thumbnails, so the rest are only found by swiping sideways." Eleven
  photographs behind a sideways swipe are eleven photographs most visitors never see. Three to a row is
  what a phone's own photo app does, and eleven of them is four rows somebody scrolls past anyway.

  ONE RULE, TWO SIZES. auto-fill with a minimum column width means the count follows the screen rather
  than a breakpoint deciding it: about three columns on a 390px phone, eight or nine across the 70rem
  wrap. The 40rem override only raises the minimum, so a desktop gets fewer, larger thumbnails instead
  of a wall of small ones.

  The scroll-snap, the hidden scrollbar and the 45% flex basis are gone with the strip. Keeping them
  would leave a phone with a grid AND an overflow container that can never scroll.

  aspect-ratio with object-fit:cover keeps the grid honest: the thumb variant is already square, so this
  only matters for a photograph whose variant is missing, and a ragged grid is worse than a crop.

  NO BACKTICKS ANYWHERE IN THIS FILE, including in a comment. The whole stylesheet is one template
  literal, so a backtick ends it and the build fails somewhere unrelated. It has cost seven builds; there
  is now a test that just counts them.
*/
.gallery{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(5.5rem,1fr));gap:.5rem}
.gallery__link{display:block;border-radius:var(--radius);overflow:hidden}
.gallery__img{width:100%;height:auto;aspect-ratio:1;object-fit:cover;display:block;transition:opacity .15s}
.gallery__link:hover .gallery__img,.gallery__link:focus-visible .gallery__img{opacity:.85}
@media (min-width:40rem){
  .gallery{grid-template-columns:repeat(auto-fill,minmax(7.5rem,1fr))}
}

/*
  THE VIEWER. A dialog on the top layer, so no z-index on the page can cover it, and ::backdrop is what
  dims everything behind rather than a div pretending to.

  The buttons are large and have real hit areas because this is used with a thumb on a phone. They sit
  above the photograph in the stacking order and are translucent, so a dark photograph does not swallow
  them and a light one does not either.
*/
.lightbox{border:0;padding:0;max-width:100vw;max-height:100vh;width:100vw;height:100vh;background:transparent;display:none}
.lightbox[open]{display:grid;place-items:center}
.lightbox::backdrop{background:rgba(0,0,0,.9)}
.lightbox__img{max-width:94vw;max-height:88vh;width:auto;height:auto;object-fit:contain;border-radius:var(--radius)}
.lightbox__btn{position:fixed;border:0;cursor:pointer;background:rgba(0,0,0,.55);color:#fff;font-size:1.75rem;line-height:1;width:3rem;height:3rem;border-radius:999px;display:grid;place-items:center}
.lightbox__btn:hover{background:rgba(0,0,0,.8)}
.lightbox__prev{left:.75rem;top:50%;transform:translateY(-50%)}
.lightbox__next{right:.75rem;top:50%;transform:translateY(-50%)}
.lightbox__close{right:.75rem;top:.75rem;font-size:1.25rem}

/*
  THE BOOKING MODAL.

  ══════════════════════════════ WHY IT IS A MODAL AND NOT A PAGE ═══════════════════════════════

  /api/book is still a whole working page and every Book link still points at it -- see
  BOOK_MODAL_SCRIPT for why that matters. But that page is served by the API with only the site's
  stylesheet to go on: no header, no navigation, nothing to say whose website it is. Somebody who
  taps "Book a session" and lands on a bare page with a list of times has left the website as far as
  they can tell, and the way back is the browser's back button.

  So the times come to the page instead. A dialog element on the top layer, so no z-index on any
  block can cover it, and its ::backdrop dims the site rather than a div pretending to.

  ══════════════════════════════ A SHEET ON A PHONE, A CARD ON A DESKTOP ════════════════════════

  Under 34rem it is full width and pinned to the bottom, because that is where a thumb is and because
  a centred card on a 360px screen is a card with 8px margins. Above that it is a centred column with
  a maximum width -- a month grid stretched across a 1400px monitor has 200px cells and reads as a
  spreadsheet.

  ══════════════════════════════ GREEN AND RED, AND NOT ONLY GREEN AND RED ══════════════════════

  These sites theme themselves from the client's own palette, so a tint that reads correctly on a
  cream studio site is invisible on a charcoal one. Hence FILLED chips with white text and a brighter
  edge: the fill carries the meaning on any ground, and the ratios (6.4:1 and 6.1:1) hold whatever
  the page behind is doing.

  Colour is never the only signal. Every day cell carries an aria-label saying how many times are
  free or that it is fully booked, a full day is not a button at all, and there is a legend under the
  grid -- so the calendar still works for somebody who cannot tell the two hues apart.
*/
/*
  ═════════════════════════════ NOT 100vw, AND WHY THAT MATTERS ═══════════════════════════════

  width:100vw on a modal is a trap: on a desktop 100vw INCLUDES the scrollbar gutter, so the dialog
  is a scrollbar wider than the space it has and the page gains a horizontal scrollbar of its own.
  inset:0 with margin:0 asks for the viewport instead of measuring it, which is both shorter and
  cannot be wrong.

  Heights are declared twice on purpose: dvh is the one that is right on a phone, where the address
  bar comes and goes, and vh is there for anything that does not know dvh yet. The later declaration
  wins where it is understood.
*/
.bookmodal{position:fixed;inset:0;margin:0;width:auto;height:auto;max-width:none;max-height:none;border:0;padding:0;background:transparent;display:none}
.bookmodal[open]{display:flex;align-items:flex-end;justify-content:center}
.bookmodal::backdrop{background:rgba(0,0,0,.6)}
.bookmodal__box{
  position:relative;width:100%;max-width:34rem;
  max-height:92vh;max-height:92dvh;
  overflow-y:auto;-webkit-overflow-scrolling:touch;
  background:var(--surface);color:var(--ink);
  border-radius:var(--radius) var(--radius) 0 0;
  padding:1rem 1rem calc(1rem + env(safe-area-inset-bottom));
}
@media (min-width:34rem){
  .bookmodal[open]{align-items:center}
  .bookmodal__box{border-radius:var(--radius);max-height:88dvh;padding:1.25rem}
}
/* 44px, top right, and outside the scrolling content so it is reachable however far down they are. */
.bookmodal__close{
  position:sticky;top:0;float:right;margin:-.25rem -.25rem 0 .5rem;
  width:44px;height:44px;border:0;border-radius:999px;cursor:pointer;
  background:var(--surface-alt);color:var(--ink);font-size:1.1rem;line-height:1;
  display:grid;place-items:center
}
.bookmodal__title{margin:0 0 .25rem;font-size:1.2rem}
.bookmodal__what{margin:0 0 1rem;color:var(--ink-muted);font-size:.95rem}
.bookmodal__msg{margin:.75rem 0}
/* A refusal from the endpoint, above the form it belongs to, in the site's own error treatment. */
.bookmodal__problem{margin:0 0 .75rem;padding:.6rem .75rem;border:2px solid #b3261e;border-radius:var(--radius);background:rgba(179,38,30,.1)}

.bookmodal__month{display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin-bottom:.5rem}
.bookmodal__month strong{font-size:1.05rem}
.bookmodal__nav{
  width:44px;height:44px;flex:0 0 44px;
  border:1px solid var(--ink-muted);border-radius:var(--radius);
  background:none;color:var(--ink);font:inherit;cursor:pointer;display:grid;place-items:center
}
.bookmodal__nav[disabled]{opacity:.35;cursor:default}

/*
  ═════════════════════════════ WHY minmax(0,1fr) AND NOT 1fr ═════════════════════════════════

  1fr is minmax(auto,1fr), and auto refuses to go below the item's own minimum -- which, for a square
  cell with a min-height, is that height. Seven of those plus six gaps came to 304px inside a 280px
  grid on a 320px phone, and the last column hung off the side of the screen. Measured, not guessed:
  a probe reported five list items with a right edge past the viewport.

  minmax(0,1fr) lets the columns shrink, which is what a seven-column grid on a small phone has to be
  allowed to do. aspect-ratio keeps them square at whatever width they end up.
*/
.bookmodal__dows{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.25rem;text-align:center;font-size:.7rem;letter-spacing:.02em;color:var(--ink-muted)}
.bookmodal__grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:.25rem;margin:.25rem 0 .75rem;padding:0;list-style:none}

/*
  ═════════════════════════════ ONE SHAPE, WHICHEVER ELEMENT IT IS ════════════════════════════

  A free day is a button and a full day is a span, because a day with nothing left must not invite a
  tap that can do nothing. Left to the browser those are two different boxes: the span stretches to
  its grid column and the button does not, so the full day rendered at 68px beside a 40px free one on
  a wide screen -- which is what the calendar was reported to look like. width:100% and
  box-sizing:border-box make the geometry identical, and the difference purely what is drawn inside.

  min-height:2rem is a floor rather than a target -- 32px, seven of which fit a 320px screen with
  room to spare. It is under the 44px this project usually insists on, deliberately: a month is seven
  columns wide and there is no arrangement of seven 44px targets that fits a small phone. The times
  below it, which are the thing actually being chosen, are full-height chips.
*/
.bm-day{
  box-sizing:border-box;width:100%;aspect-ratio:1;min-height:2rem;padding:0;
  display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.2rem;
  border:1px solid transparent;border-radius:var(--radius);
  background:transparent;color:var(--ink);
  font:inherit;font-size:.95rem;line-height:1;font-variant-numeric:tabular-nums
}
.bm-num{display:block}

/*
  THE BAR IS THE STATE.

  It used to be the whole cell filled green or red, which read as a heat map rather than a calendar
  and was loud enough to be the only thing on the page. A 3px bar under the number says the same
  thing quietly, and the day's own number stays readable in the site's own ink.

  The two hues are mid-tone rather than dark, because a client site themes itself from the client's
  own palette: a deep green that reads on cream is nearly invisible on charcoal. Colour is never the
  only signal -- a full day is not a button, every day carries an aria-label saying how many times
  are free or that it has gone, and the legend names both.
*/
.bm-bar{display:inline-block;width:1.1rem;height:.2rem;border-radius:999px;background:transparent}
.bm-bar--free,.bm-day--free .bm-bar{background:#2f9e44}
.bm-bar--full,.bm-day--full .bm-bar{background:#e03131}

/* Something to tap: a faint ground so the free days read as a group, and a brass edge on hover. */
.bm-day--free{background:var(--surface-alt);cursor:pointer}
.bm-day--free:hover{border-color:var(--brand)}
/* Gone. Quieter, and inert. */
.bm-day--full{color:var(--ink-muted)}
/* Been and gone, shut, or past the horizon. Present, plainly not on offer. */
.bm-day--past,.bm-day--shut{color:var(--ink-muted);opacity:.5}
/* The day being looked at. Filled, because it has to win against a row of tinted cells. */
.bm-day--on{background:var(--brand);border-color:var(--brand);color:var(--brand-ink);font-weight:700}
.bm-day--on .bm-bar{background:var(--brand-ink)}

.bookmodal__legend{margin:0 0 1rem;font-size:.8rem;color:var(--ink-muted);display:flex;flex-wrap:wrap;align-items:center;gap:.35rem}
.bookmodal__legend .bm-bar{margin-left:.5rem}
.bookmodal__legend .bm-bar:first-child{margin-left:0}

/* The times for the chosen day. A wrapping row of 44px-tall chips, which is what a thumb wants. */
.bookmodal__times{display:flex;flex-wrap:wrap;gap:.5rem;margin:.5rem 0 1rem;padding:0;list-style:none}
.bookmodal__time{
  min-height:44px;padding:.5rem .9rem;font:inherit;cursor:pointer;
  background:transparent;color:var(--brand-text);border:2px solid var(--brand-text);border-radius:var(--radius)
}
.bookmodal__time--on{background:var(--brand);border-color:var(--brand);color:var(--brand-ink);font-weight:700}
.bookmodal__chosen{margin:0 0 .75rem;font-weight:700}
.bookmodal__form .field{margin-bottom:.85rem}
.bookmodal__form .btn{width:100%}
@media (min-width:40rem){.grid--2{grid-template-columns:repeat(2,1fr)}}
@media (min-width:56rem){.grid--3{grid-template-columns:repeat(3,1fr)}}

.card{background:var(--surface);border:1px solid var(--surface-alt);border-radius:var(--radius);padding:1.25rem}
.card__price{font-weight:700;color:var(--brand-text)}

/*
  MOVED HERE, AFTER THE CARD RULE, AND THAT IS THE WHOLE REASON THIS COMMENT EXISTS.

  These were two hundred lines earlier, next to the other journal rules -- and the .card rule with its
  1.25rem padding sits below, at the same specificity, so it won. The photograph came out 305px wide in
  a 347px card: flush on the left, forty pixels of cream on the right, because width:100% was resolving
  against a padded content box. Measured in the browser rather than eyeballed -- the screenshot showed
  only that something was off.

  A more specific selector would also have worked and would have been worse: the next person to add a
  card variant would have had to know to out-specify a rule further down the file.
*/
/*
  THE JOURNAL INDEX AND THE HOME-PAGE TEASER, as cards.

  It was one column of bold links capped at 42rem, so on a desktop it used a third of the page and
  read as a sitemap -- six of them on a journal index looked like an appendix rather than like the
  writing being the point. The cards reuse the grid and card classes the services block uses, so the
  journal matches the rest of the site instead of inventing a second card style.

  A column flex box with the blurb pushed down by margin-top:auto is what keeps a row tidy when one
  article has a two-line title and the next has one: the descriptions still line up along the bottom,
  so the row reads as a row.
*/
.card--article{display:flex;flex-direction:column;padding:0;overflow:hidden}
/* The picture is flush to the card's edges, so the radius has to come off the image itself. */
.card--article .card__img{width:100%;aspect-ratio:3/2;object-fit:cover;border-radius:0;margin:0}
/*
  THE WHOLE PICTURE IS THE TARGET. line-height:0 because an inline anchor around a block image
  otherwise leaves a few pixels of text leading under it, which shows as a stripe of card colour
  between the photograph and the title.
*/
.card__media{display:block;line-height:0}
/* Nothing on hover of its own: the title's underline is the card's one feedback, and two at once
   reads as two links. */
.card__more{white-space:nowrap;text-decoration-color:var(--brand)}
.card__title{margin:1rem 1.25rem 0;font-size:1.05rem;line-height:1.3}
/*
  A SERVICES CARD IS PADDED, an article card is not, so a heading inside one must not take the article
  card's inset margins on top of the padding it already has. Same link colours, no double gutter.
*/
.card__title--plain{margin:0 0 .5em;font-size:1.2rem}
.card__title a{color:var(--ink);text-decoration:none}
.card__title a:hover{text-decoration:underline;text-decoration-color:var(--brand)}
.card--article .articles__blurb{margin:.5rem 1.25rem 1.25rem;margin-top:auto;padding-top:.5rem}

.quote{margin:0;padding:1.25rem;background:var(--surface-alt);border-radius:var(--radius)}
.quote__text{margin:0 0 .5rem}
.quote__who{font-style:normal;font-weight:600}

.hours{list-style:none;margin:0;padding:0}
.hours li{display:flex;justify-content:space-between;gap:1rem;padding:.35rem 0;border-bottom:1px solid var(--surface-alt)}

.areas{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:.5rem}
.areas li{background:var(--surface-alt);border-radius:var(--radius);padding:.35rem .75rem}

.faq details{border-bottom:1px solid var(--surface-alt);padding:.75rem 0}
.faq summary{cursor:pointer;font-weight:600;min-height:44px;display:flex;align-items:center}

.accred{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:1rem;align-items:center}
.accred li{display:flex;align-items:center;gap:.5rem}

.field{display:block;margin-bottom:1rem}
.field__label{display:block;font-weight:600;margin-bottom:.25rem}
.field input,.field textarea{
  width:100%;min-height:48px;padding:.6rem .75rem;
  font:inherit;color:var(--ink);
  background:var(--surface);border:1px solid var(--ink-muted);border-radius:var(--radius);
}
.field textarea{min-height:8rem;resize:vertical}

/*
  THE FOOTER, which now carries the things a footer is for: where to find them, how to reach them, and
  the two legal pages. Four columns on a desktop, stacked on a phone, with auto-fit rather than a
  fixed count, so a client with no social accounts does not leave a hole where that column was.
*/
.site-footer{border-top:1px solid var(--surface-alt);background:var(--surface-alt);margin-top:3rem;padding-block:2.5rem 1.5rem}
.site-footer__cols{display:grid;gap:1.75rem;grid-template-columns:repeat(auto-fit,minmax(13rem,1fr))}
.site-footer h2{font-size:.8rem;text-transform:uppercase;letter-spacing:.05em;color:var(--ink-muted);margin:0 0 .5rem}
.site-footer ul{list-style:none;margin:0;padding:0}
.site-footer li{margin:0 0 .35rem}
.site-footer a{color:var(--ink);text-decoration:none}
.site-footer a:hover{text-decoration:underline}
.site-footer address{font-style:normal;color:var(--ink-muted);line-height:1.6}
/*
  ── THE MARK IN THE FOOTER ──────────────────────────────────────────────────────────────────────

  Asked for after a client looked at their own site: "there's a big empty space I want to fill". The
  footer's columns are uneven by nature -- Pages has nine links and Follow has one -- so the taller the
  menu grows, the more empty ground the other three columns leave beside them.

  ANCHORED BOTTOM-RIGHT, which is where the emptiness actually is: the long column is the first one,
  so the space opens on the opposite side. Absolute inside the footer's own wrap, so it fills that
  ground without taking part in the layout -- the columns and the legal line sit exactly where they did
  and nothing moves as the logo loads.

  FADED AND BEHIND EVERYTHING. It is scenery, not a second logo competing with the header's: opacity
  keeps it under the text it shares space with, and z-index 0 against the content's 1 means a link
  passing over it stays readable and clickable. pointer-events:none so it can never eat a tap meant for
  a link near it.

  CLIPPED BY THE FOOTER, which is what makes an "enlarged" mark safe. It is sized against the footer
  rather than the viewport and the wrap hides its overflow, so it cannot widen the page -- the failure
  that a decorative oversized image invites, and the one this project has already paid for on a phone.

  GONE ON A NARROW SCREEN. Below 48rem the columns stack, there is no empty ground to fill, and a
  watermark behind stacked text is just text that is harder to read.
*/
.site-footer .wrap{position:relative;overflow:hidden}
.site-footer__cols,.site-footer__legal{position:relative;z-index:1}
.site-footer__mark{
  position:absolute;right:-1rem;bottom:-2rem;z-index:0;
  width:auto;height:clamp(9rem,22vw,16rem);
  opacity:.10;
  /* The logo's own colours would compete; one flat tone reads as a watermark rather than a second mark. */
  filter:grayscale(1);
  pointer-events:none;user-select:none
}
@media (max-width:47.99rem){.site-footer__mark{display:none}}
/*
  THE DIVIDER ABOVE THE LEGAL LINE, and it was drawn in the wrong colour.

  It was var(--surface) -- the PAGE background -- on a footer painted var(--surface-alt). Those two
  tokens have no guaranteed relationship, and on a real client's palette the page was the lighter of
  the two (#faf5f0 against #f6ede5), so the divider came out lighter than the thing it sat on: a white
  line across the footer. Reported once the mark went in behind it, which is what made it obvious.

  DEFINED AGAINST THE INK INSTEAD. A divider wants to be a shade of whatever it lies on, not another
  surface colour, so it darkens the footer by a fixed amount whatever the brand palette is. The
  color-mix line follows the theme's own ink, which keeps it correct if a dark footer ever exists; the
  plain rgba above it is what a browser without color-mix uses, and it is right for every palette this
  platform generates today.
*/
.site-footer__legal{
  margin-top:2rem;padding-top:1rem;
  border-top:1px solid rgb(0 0 0 / .08);
  border-top-color:color-mix(in srgb, var(--ink) 14%, transparent);
  display:flex;flex-wrap:wrap;gap:.5rem 1.25rem;align-items:baseline;
  color:var(--ink-muted);font-size:.85rem
}
.site-footer__legal a{color:var(--ink-muted)}

@media print{.site-header,.nav,.btn{display:none}}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* ═══════════════════════════════ THE BOLD TEMPLATE ═══════════════════════════════════════════════

   Everything below applies only under [data-template=bold], which the renderer emits only when the
   document asks for it. A site that does not ask is not affected by a single rule here -- verified by
   diffing two live sites across the change rather than by reasoning about specificity.

   WHAT IT CHANGES, AND WHY THESE FOUR THINGS. Put beside a real business's own site, the gap was never
   colour: it was SHAPE. One measured light column with the photograph under the words, against a
   full-bleed hero with the navigation over it and alternating dark bands. So:

     1. the header lies ON the hero rather than above it
     2. the hero photograph is full-bleed and the words sit on it
     3. sections alternate dark and light instead of running one shade
     4. body copy is two columns, heading left, prose right

   NO NEW MARKUP. Every rule works against the HTML the plain template already emits, which is why this
   could ship without touching the renderer's structure or any of its tests.
*/

[data-template=bold]{--measure:64rem}

/* ── 1. THE HEADER, LAID OVER THE HERO ──────────────────────────────────────────────────────────
   Absolute rather than fixed: a fixed header on a phone eats a fifth of a small screen for the whole
   scroll, and this one only needs to be transparent where it overlaps the photograph.
*/
/*
  ══════ THE SELECTORS HERE WERE WRONG AND THE MENU WAS BLACK ON A BLACK PHOTOGRAPH ════════════════

  The first version styled .nav__link and .nav__summary. NEITHER CLASS EXISTS. The markup is a bare
  anchor inside .nav__list li, and the services dropdown is .nav__label. So every rule matched
  nothing, the links kept the default dark ink, and they were laid over a dark hero -- invisible, which
  is how it was reported.

  Written against the rendered HTML this time rather than against remembered class names.
*/
/*
  IT FLOATS WITH THE SCROLL, which is what position:sticky gives and absolute did not. The base
  template is already sticky; the bold override had pinned it to the top of the document instead, so it
  scrolled away with the hero.

  A TRANSLUCENT BAND RATHER THAN NOTHING. Fully transparent works over the hero and nowhere else: once
  the page scrolls, a sticky header with no background sits over body copy and the two sets of words
  overlap. There is no JavaScript on these pages to swap a class at a scroll position, and adding some
  for decoration would be the wrong trade -- so the band is always there. Over the photograph it reads
  as part of the hero's scrim; over content it is a header.
*/
/*
  ══════ FIXED, SO THE PHOTOGRAPH STARTS AT THE TOP OF THE PAGE ═══════════════════════════════════

  "Push the hero image on homepage all the way to the top."

  A STICKY HEADER STILL OCCUPIES ITS ROW. That is the whole difference between sticky and fixed, and it
  is what put a dark band above the photograph: the hero began below the header instead of behind it.
  The bold template's first note says "the header lies ON the hero rather than above it", and since the
  header became sticky that had quietly stopped being true.

  FIXED RATHER THAN BACK TO ABSOLUTE. Absolute is what it was originally, and it is what made the
  header scroll away with the hero -- reported as "make the header icon + menu float with scroll".
  Fixed is the one position that does both: out of the flow, so the photograph starts at the top, and
  pinned to the viewport, so it is still there on the tenth section.

  THE PAGES THAT DO NOT LEAD WITH A PHOTOGRAPH GET THE ROOM BACK. Every page shape this renders leads
  with a hero except one -- the generated journal index, which leads with a text block -- and on that
  page a fixed header with nothing behind it would sit on top of the first heading. The :has() selector asks the
  question directly rather than assuming, so a future page shape without a hero is covered too.
*/
[data-template=bold] .site-header{position:fixed;inset-inline:0;top:0;z-index:20;background:rgb(0 0 0 / .72);backdrop-filter:blur(8px);border-bottom:0}
[data-template=bold] main:not(:has(> .hero:first-child)){padding-block-start:5rem}
/*
  ══════ LIGHTER AT THE TOP OF THE PAGE, SETTLING AS IT SCROLLS ═══════════════════════════════════

  "Make the header menu background slightly transparent when scrolled all the way up."

  The band has one opacity everywhere, and it was picked for the hard case: a sticky header over body
  copy, where anything lighter lets two sets of words overlap. Over the HERO that same value is heavier
  than it needs to be -- there is a photograph under it, a scrim over that, and white text on top, so
  the band can be much lighter and still read.

  THE COMMENT ABOVE SAID THIS COULD NOT BE DONE. "There is no JavaScript on these pages to swap a class
  at a scroll position, and adding some for decoration would be the wrong trade." That was true when it
  was written and stopped being true when the reveal-on-scroll went in: animation-timeline:scroll()
  ties an animation to the document's own scroll offset, with no observer and no script.

  SO THE FALLBACK IS THE OLD BEHAVIOUR, EXACTLY. A browser without scroll-driven animations -- Safari
  and Firefox today -- keeps the .72 band at every position, which is what shipped before and is never
  wrong, only heavier than it needs to be for the first screen. Same under prefers-reduced-motion,
  where the blanket rule near the top of this sheet switches every animation off.

  220px rather than the full hero: by then the header has left the photograph on any viewport this
  renders to, and the band has to be at its full weight before it meets the first paragraph.
*/
@supports (animation-timeline:scroll()){
  @media (prefers-reduced-motion:no-preference){
    [data-template=bold] .site-header{
      animation:header-settle linear both;
      animation-timeline:scroll(root block);
      animation-range:0 220px
    }
  }
}
@keyframes header-settle{
  from{background-color:rgb(0 0 0 / .18)}
  to{background-color:rgb(0 0 0 / .72)}
}
[data-template=bold] .site-header__name,[data-template=bold] .site-header__wordmark{color:#fff}
/*
  ══════ WHITE, BOLD, AND NOT OUTLINED -- AND ONLY WHERE THE BAND IS BEHIND IT ════════════════════

  THE OUTLINE IS GONE. It was two tight black offsets plus a halo, added when the header was
  transparent and the links lay directly on a photograph. The header is a translucent black band now,
  so white already reads on it, and the ring on top of a solid ground just looked smudged:
  "remove the black outline on the menu items, leave it white text only".

  WHITE IS NOW SCOPED TO THE DESKTOP BAR, and that is a fix rather than tidying. The previous rule was
  unconditional, so it also coloured the links inside the MOBILE DRAWER -- which is a panel painted
  var(--surface), a near-white. Every item in the phone menu was white on white. Nobody reported it
  because the reported bug was the same colour clash one level down, in the services dropdown, and
  they have the same cause.

  Below 48rem the links simply keep var(--ink) from the base template, which is what the drawer wants.
*/
@media (min-width:48rem){
  [data-template=bold] .nav__list>li>a,[data-template=bold] .nav__label{color:#fff;font-weight:700}
  [data-template=bold] .nav__list>li>a[aria-current]{color:#fff;text-decoration-color:var(--brand)}
}
/*
  ══════ THE SERVICES DROPDOWN, WHICH WAS WHITE TEXT ON A WHITE PANEL ═════════════════════════════

  Reported with a photograph of it: the panel opens, and the one item in it is a pale ghost you can
  only see because it is slightly less white than the card behind it.

  The old rule was .nav__list a -- a DESCENDANT selector -- and the dropdown items are anchors inside
  .nav__sub inside .nav__list, so they inherited the white meant for links lying on the hero. But the
  panel is not on the hero: it is an opaque var(--surface) card hanging below the bar. Only the
  text-shadow was being undone for it, which removed the outline and left the invisible white.

  Two changes, either of which would fix it and both of which are worth having: the white is now a
  CHILD selector that cannot reach into the panel, and the panel's own items say what colour they are
  rather than relying on not being overridden.
*/
[data-template=bold] .nav__sub a{color:var(--ink);text-shadow:none;font-weight:600}
[data-template=bold] .nav__sub a:hover,[data-template=bold] .nav__sub a:focus-visible{background:var(--surface-alt);color:var(--brand-text)}
[data-template=bold] .nav__toggle{color:#fff;border-color:rgb(255 255 255 / .55);font-weight:700}
/*
  THE MENU BUTTON GOES TO THE RIGHT EDGE on a phone. justify-content:space-between spreads three
  children -- the lockup, the button and the nav -- so the button landed between the other two rather
  than at the end. An auto left margin pushes it against the edge whatever else is in the row.
*/
[data-template=bold] .nav__toggle{margin-left:auto}
/* The scrim the header sits on, so white text survives a pale photograph. */
[data-template=bold] .hero::before{content:"";position:absolute;inset-inline:0;top:0;height:9rem;background:linear-gradient(rgb(0 0 0 / .45),transparent);z-index:1;pointer-events:none}

/* ── 2. THE HERO, FULL BLEED, WITH THE WORDS ON THE PHOTOGRAPH ──────────────────────────────────
   The breakout is the standard one: pull the section out to the viewport edges and put the padding
   back inside. The photograph is taken out of flow and placed behind, so the markup order -- heading,
   standfirst, button, media -- is unchanged and only the painting differs.
*/
/*
  THE INLINE PADDING DOES THE COLUMN, which is why no child needs a max-width or an auto margin. A
  centred measure built from auto margins works for block children and does nothing for the inline-block
  button, so the call to action sat at the viewport edge while the words sat in the column -- or, once
  centred, in the middle of the page under left-aligned text. Padding moves the whole content box.
*/
[data-template=bold] .hero{position:relative;isolation:isolate;margin-inline:calc(50% - 50vw);padding:9rem max(1.25rem,calc((100% - var(--measure)) / 2)) 4.5rem;max-width:none;color:#fff;overflow:hidden;display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-end;min-height:clamp(28rem,66vh,42rem)}
[data-template=bold] .hero__media,[data-template=bold] .hero__slider{position:absolute;inset:0;margin:0;max-width:none;border-radius:0;z-index:-2}
[data-template=bold] .hero__img{width:100%;height:100%;object-fit:cover}
/* A second scrim over the whole photograph: the heading has to be readable over any frame of it. */
[data-template=bold] .hero::after{content:"";position:absolute;inset:0;z-index:-1;background:linear-gradient(90deg,rgb(0 0 0 / .72),rgb(0 0 0 / .25))}
[data-template=bold] .hero__heading,[data-template=bold] .hero__sub{max-width:48rem;color:#fff;position:relative;z-index:2}
[data-template=bold] .hero__heading{font-size:clamp(2.2rem,5.2vw,3.6rem);line-height:1.08;letter-spacing:-.02em}
[data-template=bold] .hero__sub{color:rgb(255 255 255 / .86);font-size:1.2rem}
[data-template=bold] .hero .btn{position:relative;z-index:2;margin-top:.5rem}

/* ── 3. ALTERNATING BANDS ───────────────────────────────────────────────────────────────────────
   nth-of-type on the sections rather than a class, because the renderer decides which blocks exist and
   the rhythm should not depend on which ones it chose. Every second section is dark.
*/
[data-template=bold] .section{padding-block:4.5rem}
[data-template=bold] .section:nth-of-type(even){background:var(--ink);color:#fff}
[data-template=bold] .section:nth-of-type(even) h2,[data-template=bold] .section:nth-of-type(even) h3{color:#fff}
[data-template=bold] .section:nth-of-type(even) p,[data-template=bold] .section:nth-of-type(even) li{color:rgb(255 255 255 / .82)}
[data-template=bold] .section:nth-of-type(even) .card{background:rgb(255 255 255 / .06);border-color:rgb(255 255 255 / .14)}
[data-template=bold] .section:nth-of-type(even) .card a{color:#fff}
[data-template=bold] .section--alt{background:var(--surface-alt)}
[data-template=bold] .section:nth-of-type(even).section--alt{background:var(--ink)}

/* ── 4. THE ACCENT RULE, AND TWO COLUMNS ────────────────────────────────────────────────────────
   A short brand-coloured bar above a section heading. It is the cheapest thing on their site to
   reproduce and does more for the impression of design than anything else here.
*/
/*
  THE PLACES THEY COVER, AS BRAND-COLOURED TAGS. On the plain template these sit on --surface-alt, a
  near-white that is nearly invisible on a white band -- "make the cities tags more visible, perhaps as
  orange background tags". --brand-text rather than white, because a brand colour can be pale and a
  fixed white would be unreadable on it; that token already carries whichever of black or white reads.
*/
[data-template=bold] .areas li{background:var(--brand);color:var(--brand-text);font-weight:600;padding:.4rem .85rem}
[data-template=bold] .section:nth-of-type(even) .areas li{color:var(--brand-text)}

[data-template=bold] .section h2::before{content:"";display:block;width:4.5rem;height:.4rem;background:var(--brand);margin-bottom:1.1rem}
[data-template=bold] .section h2{font-size:clamp(1.8rem,3.4vw,2.6rem);line-height:1.12;letter-spacing:-.015em}

/*
  THE BODY SPLIT, and the grid is only asked for where there is something to split. A prose block with
  no heading stays one column -- a lone paragraph in the right-hand half of a page, with a gap where a
  heading would be, looks like a mistake rather than a layout.
*/
@media (min-width:56rem){
  /*
    THE HEADING IS TAKEN OUT OF FLOW, and two earlier versions explain why.

    FIRST it was a grid with the heading spanning ninety-nine rows so it would sit beside the whole of
    the prose. Grid honours that literally: ninety-nine rows, ninety-odd empty, and the page grew a black
    void several screens tall with the rest of the site below it.

    THEN it was a grid with the heading in row one. No void -- but a grid row is as tall as its tallest
    cell, and a three-line heading made row one three lines tall, so the first paragraph beside it sat
    against the top of a deep row with a gap underneath. Visible immediately on a real page: a subheading
    reading "Dual expertise" floating a long way above its own paragraph.

    A heading placed ABSOLUTELY in a left-hand gutter has neither problem. The prose stays one ordinary
    column, indented, and the heading sits beside its start. No rows to size, none to leave empty, and
    the text flows exactly as it does on every other template.
  */
  [data-template=bold] .prose:has(h2){position:relative;padding-left:38%;max-width:none;min-height:1px}
  /*
    ONLY THE FIRST HEADING BECOMES THE SIDENOTE, and the first version said every h2.

    A prose block carries the block's own heading AND whatever headings the body markdown contains --
    renderRestrictedMarkdown emits h2 and h3 -- so a section with a heading of its own and a couple of
    markdown headings has three h2 elements in one container. Positioning them all absolutely puts every
    one of them at the top-left of the same box: on the rebuilt site "Why retailers work with us" was
    painted directly on top of "An independent partner for retail at scale", both illegible.

    The later ones stay in flow, where the container's left padding already places them in the text
    column beside the sidenote -- which is where a subheading belongs anyway.
  */
  [data-template=bold] .prose:has(h2)>h2:first-of-type{position:absolute;left:0;top:0;width:34%;margin:0}
  [data-template=bold] .prose:has(h2)>h2:not(:first-of-type){position:static;width:auto}
}

/*
  ══════ THE FOOTER MARK: BIG, WHOLE, AND ABOVE THE RULE ══════════════════════════════════════════

  "Add the logo in the footer, high res, as background to cover the blank spaces" put one there. The
  follow-up -- "the logo in the footer doesn't look good, and it goes over a gray horizontal line" --
  is three separate faults in the version that shipped, and each is fixed here or alongside.

  IT WAS UPSCALED, badly. The renderer resolves the mark through card -> thumb -> touch_icon, and the
  imported logo was a 254x151 webp -- too narrow for any page size, so the only variants that existed
  were the two ICONS. It fell through to touch_icon: a 180-pixel PNG, blown up to 480, of a wordmark
  sitting inside a transparent square. The real file on their old site is 757x450; re-importing that
  is the other half of this fix and is what makes card exist. The height here is capped at what that
  variant can fill without stretching.

  IT WAS CLIPPED. right:-3rem hung it off the edge, and the wrap's overflow:hidden cut the last
  letters off a WORDMARK. A cropped photograph is a crop; a cropped wordmark is a spelling mistake.

  IT CROSSED THE DIVIDER. The rule above the legal line is a real border on a z-index:1 element, so it
  painted straight across the mark. bottom is now clear of that block -- its own height is the 1rem of
  padding plus one line -- so the mark sits in the empty ground beside the columns and stops above the
  rule instead of being sliced by it.

  It keeps every protection the plain one has: clipped by the wrap so it cannot widen the page, behind
  the columns, ignored by the pointer, and gone below 48rem where the columns stack and there is no
  empty ground to fill.

  NOT GREYSCALED HERE, unlike the plain template. That filter exists to stop a full-colour logo
  competing with the page, and the mark this resolves to is assets.footerLogo -- the version the
  client drew for a light ground, which for most businesses is already a single flat tone. Draining a
  monochrome mark changes nothing and only risks flattening one that is deliberately two-tone.

  The opacity does the watermarking instead: a dark mark at .14 over a pale band is a soft grey behind
  the columns rather than a second logo competing with the one in the header.
*/
[data-template=bold] .site-footer__mark{height:clamp(11rem,24vw,17rem);right:1.5rem;bottom:4rem;opacity:.14;filter:none}

/*
  ══════ THE FOOTER STAYS LIGHT, AND THE MARK IS THE CLIENT'S DARK VERSION ════════════════════════

  IT WAS DARK FOR ONE DEPLOY, and the reasoning was sound as far as it went: this client's mark is
  white lettering, a white mark needs a dark ground, and their own site's footer is an orange band
  carrying that exact white wordmark over a black one. So the band went dark to suit the artwork.

  THEN THE ARTWORK CHANGED. They supplied a DARK version of the mark specifically for the footer --
  "for the footer logo use the one attached" -- which is the other half of a normal brand pair and
  wants a light ground. Painting the band dark now would hide it exactly as the white one was hidden.

  So the band keeps the base template's surface and assets.footerLogo carries the version that reads
  on it. Which is the whole point of that slot existing: the document says which mark suits which end
  of the page, and the stylesheet stops having to guess.
*/

/*
  ══════ THE CLIENTS THEY HAVE WORKED WITH ════════════════════════════════════════════════════════

  "Clients we've worked with -- with their logos, grayscale by default, coloured on mouse over, no
  click action."

  THE BLOCK ALREADY EXISTS. accreditations is {heading, items:[{name, logo}]} and renders each item
  as a picture and a label inside a plain li -- NO anchor, so "no click action" is what it already
  does and nothing here has to take a link away.

  GREYSCALE IS THE RIGHT DEFAULT FOR BORROWED MARKS. A row of other companies' logos in full colour
  competes with the client's own brand and reads as advertising for them; drained to one tone it reads
  as a list of names. Colour on hover gives the visitor the real mark when they look for it.

  The label is hidden on the bold template only: their own site shows the marks alone, and a logo that
  spells the company name with the name printed underneath says it twice. It stays in the markup for
  screen readers and for every other template.
*/
/*
  SPREAD ACROSS THE MEASURE rather than bunched at the left edge. Five marks at three-and-a-bit
  centimetres each fill about two thirds of the column, and left-aligned they left a third of the row
  empty -- which reads as a list that ran out rather than a row of clients. space-between pins the
  first and last to the column edges, which is how a logo strip is normally set.

  It only applies while they FIT on one line: once the row wraps on a narrow screen, flex-wrap starts a
  second line and space-between on a trailing line of one or two marks would fling them apart, so the
  gap does the work below that width.
*/
[data-template=bold] .accred{gap:1.5rem 2.5rem;align-items:center}
@media (min-width:48rem){
  [data-template=bold] .accred{justify-content:space-between;flex-wrap:nowrap}
}
[data-template=bold] .accred li>span{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
[data-template=bold] .accred img{
  max-height:3.25rem;width:auto;
  filter:grayscale(1);opacity:.62;
  transition:filter .22s ease,opacity .22s ease,transform .22s ease
}
[data-template=bold] .accred li:hover img{filter:grayscale(0);opacity:1;transform:scale(1.04)}
/* On the dark bands the marks need lifting rather than draining, or they disappear into the ground. */
[data-template=bold] .section:nth-of-type(even) .accred img{filter:grayscale(1) brightness(1.9)}
[data-template=bold] .section:nth-of-type(even) .accred li:hover img{filter:grayscale(0)}

/* ══════════════════════════ MOVEMENT: HOVER, AND REVEAL ON SCROLL ══════════════════════════════

   "Add a nice hover effect on the cities. Also add more hover/scroll effects throughout the site."

   ALL OF IT IS INSIDE [data-template=bold]. The plain template is what every other client's site
   renders with, including one whose pages are checked against a diff after every structural change,
   and decoration is not a reason to touch them.

   NOTHING HERE MOVES THE LAYOUT. Every rule animates transform, opacity, colour or shadow -- the
   properties a browser can composite -- so nothing reflows, and a hover cannot shift the thing under
   the pointer out from under it. Sizes, margins and positions are untouched.
*/

[data-template=bold] .areas li,
[data-template=bold] .card,
[data-template=bold] .btn,
[data-template=bold] .gallery__img,
[data-template=bold] .card__img,
[data-template=bold] .site-footer a,
[data-template=bold] .nav__list>li>a,
[data-template=bold] .nav__label{
  transition:transform .18s ease,box-shadow .18s ease,background-color .18s ease,color .18s ease,opacity .18s ease
}

/*
  THE CITIES. They were made brand-coloured tags in the last round to be visible at all; the ask now
  is that they respond. They lift a little, deepen, and throw a short shadow in the brand colour --
  which reads as the tag coming forward rather than as a new colour appearing.

  cursor stays default: these are not links, and a pointer cursor on something unclickable is a worse
  lie than no hover at all.
*/
[data-template=bold] .areas li:hover{
  transform:translateY(-2px);
  background:var(--brand-text);color:var(--surface);
  box-shadow:0 .35rem .9rem rgb(0 0 0 / .22)
}
/* On a dark band the same swap has to go the other way, or the tag vanishes into the background. */
[data-template=bold] .section:nth-of-type(even) .areas li:hover{background:#fff;color:var(--ink)}

/*
  ══════ THE SERVICE CARDS LINE UP ════════════════════════════════════════════════════════════════

  "Homepage - What we do - make a fixed height for the images, so it's more aligned."

  TWO THINGS MADE THE ROW RAGGED, and fixing only the obvious one would have left it visibly ragged
  still.

  1. THE PICTURES ARE DIFFERENT SHAPES. The renderer asks for a 640-wide card variant and gets
     whatever proportion the photograph is: 640x427 for the newsstand, 640x729 for the printing press.
     Same width, seventy per cent more height. An aspect-ratio with object-fit:cover settles it, and
     cover rather than contain because a letterboxed photograph in a card looks like a mistake.
     3/2 matches what .card--article already uses for the journal, so the two grids agree.

  2. THE TITLES ABOVE THEM WRAP DIFFERENTLY. These cards are title-then-picture, so "Magazine &
     Newspaper Supply" taking two lines while "POS Printing" takes one pushes one picture a whole line
     lower than its neighbour -- and no amount of fixing the image height touches that. Reserving two
     lines on the heading is what actually aligns the row.

     min-height rather than height: a THREE-line title at a narrow width must still be allowed to be
     three lines rather than spill out of its own box.

  BOLD ONLY, like everything else in this file. .card__media is the plain template's services grid as
  well, and another client's live site is not the place to find out whether a crop suits their
  photographs.
*/
[data-template=bold] .card__media .card__img{width:100%;aspect-ratio:3/2;object-fit:cover;display:block}
[data-template=bold] .card .card__title--plain{min-height:2.6em}

/* Cards lift and gain an edge in the brand colour, so the whole tile reads as the target. */
[data-template=bold] .card:hover{transform:translateY(-3px);box-shadow:0 .75rem 1.75rem rgb(0 0 0 / .16);border-color:var(--brand)}
[data-template=bold] .section:nth-of-type(even) .card:hover{background:rgb(255 255 255 / .1);box-shadow:0 .75rem 1.75rem rgb(0 0 0 / .4)}

/*
  A SLOW ZOOM ON THE PHOTOGRAPHS. The scale lives on the image and the clipping on its link, which
  already has overflow:hidden for its corner radius -- so the picture grows inside a box that does not.
*/
[data-template=bold] .gallery__link:hover .gallery__img,
[data-template=bold] .gallery__link:focus-visible .gallery__img{transform:scale(1.07);opacity:1}
[data-template=bold] .card--article{overflow:hidden}
[data-template=bold] .card--article:hover .card__img{transform:scale(1.05)}

/* The call to action, which is the one thing on most of these pages a client wants pressed. */
[data-template=bold] .btn:hover{transform:translateY(-2px);box-shadow:0 .6rem 1.4rem rgb(0 0 0 / .25)}
[data-template=bold] .btn:active{transform:translateY(0);box-shadow:none}

/*
  THE BAR, WHERE THE HOVER IS AN UNDERLINE RATHER THAN A COLOUR.

  Brand-coloured TEXT was the obvious rule and is the wrong one twice over. The sheet's own guard
  forbids the raw brand colour as a text colour outright, because it is picked to be a background and is not
  guaranteed readable as text -- and the alternative, --brand-text, is darkened until it reads on a
  LIGHT ground, which is the opposite of the black band these links sit on.

  So the label stays white and the brand colour goes into the underline, where its only job is to be
  seen against black. It is also the same gesture the current page already uses, which makes hovering
  read as "this is where you would be".
*/
[data-template=bold] .nav__list>li>a:hover,[data-template=bold] .nav__details:hover>.nav__label{color:#fff;text-decoration:underline;text-decoration-color:var(--brand);text-decoration-thickness:3px;text-underline-offset:.35rem}
[data-template=bold] .site-footer a:hover{color:var(--brand-text)}
/*
  THE ACCENT RULE OVER A SECTION HEADING grows when the section is hovered. It is the smallest possible
  gesture and it makes a band feel like a thing rather than a stripe of colour.
*/
[data-template=bold] .section h2::before{transition:width .3s ease}
[data-template=bold] .section:hover h2::before{width:6.5rem}

/*
  ══════ REVEAL ON SCROLL, WITH NO JAVASCRIPT ══════════════════════════════════════════════════════

  animation-timeline:view() ties an animation to the element's own passage through the viewport. There
  is no observer, no script tag, and nothing to run on a page that is mostly text.

  THREE GUARDS, AND EACH ONE MATTERS.

  @supports, because a browser without scroll-driven animations would otherwise apply the keyframes as
  an ordinary animation -- or, worse, hold the from state forever. Safari and Firefox do not have it
  yet, and on those the content simply appears, which is the correct fallback for decoration.

  prefers-reduced-motion, because this is exactly the kind of movement that setting exists to switch
  off. The blanket rule near the top of this sheet disables animations under it anyway; saying so here
  as well means the from state is never even declared for someone who has asked for stillness.

  animation-fill-mode:both with a range that ENDS INSIDE THE ENTRY. An element already on screen when
  the page loads is past that range, so the fill gives it the finished state immediately -- it does not
  wait for a scroll that may never come. That is the failure mode this pattern is notorious for and it
  is worth stating: content must never be invisible because nobody scrolled.
*/
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    [data-template=bold] .section>.wrap>h2,
    [data-template=bold] .section>.wrap>p,
    [data-template=bold] .section>.wrap>.grid>*,
    [data-template=bold] .section>.wrap>.gallery>li,
    [data-template=bold] .section>.wrap>.areas>li,
    [data-template=bold] .section>.wrap>.prose,
    [data-template=bold] .section>.wrap>.quote,
    [data-template=bold] .section>.wrap>.hours{
      animation:rise-in linear both;
      animation-timeline:view();
      animation-range:entry 5% entry 55%
    }
    /*
      THE HERO IS EXEMPT. It is the first thing on the page and the only thing above the fold; fading
      it in means the site opens on a blank screen, which is the one place this effect makes a site
      feel slower rather than better.
    */
    [data-template=bold] .hero>*{animation:none}
  }
}
@keyframes rise-in{from{opacity:0;transform:translateY(1.5rem)}to{opacity:1;transform:none}}
