/* ==========================================================================
   Flosaic website - site and component styles
   Stage 4 build, 2026-08-04. Consumes assets/mozaic-webfont.css then
   assets/flosaic-tokens.css (load order matters; tokens reference the
   fallback family). Every colour, size and space comes off the token layer.

   Component sections below map 1:1 to the demos in components/:
     1 base and layout shell
     2 sidebar nav (collapses to a top bar under 768px)
     3 hero
     4 section scaffold + photo-column figure (honest placeholder until stage 5)
     5 services
     6 evidence band (stat tiles)
     7 approach strip (line-element idiom)
     8 about
     9 contact
    10 footer
   ========================================================================== */

/* --- 1. Base and layout shell --------------------------------------------- */

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

html {
  scroll-behavior: smooth;
}

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

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-body);
  font-weight: var(--weight-body);
  line-height: var(--leading-body);
  color: var(--colour-text);
  background: var(--colour-surface);
}

p {
  text-wrap: pretty;
  margin: 0 0 var(--space-3);
}

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

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

a:focus-visible {
  outline: 2px solid var(--colour-link);
  outline-offset: 2px;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

/* Sidebar width follows the Rev2 mockup's proportion: its sidebar is ~13.8%
   of the page width (258px of a 1871px canvas). The floor is the composed
   logo (~6.3rem visual) plus the inline padding.

   Rows: every section is its own grid row (main is display: contents), so
   strip elements can pin to a section's row - the About portrait pair sits
   in the About row of the strip column, overlaying the continuous strip
   (Adam, round 2 direction 2026-08-04). THE ROW COUNT IS EXPLICIT: six
   sections -> repeat(6, auto). Adding or removing a section means updating
   this and the .about-portraits grid-row below. */
/* Page cap (Adam, 2026-08-07): beyond 1808px the page stopped growing and
   only the dead space grew. The sidebar caps at 14rem, the strip column at
   24rem and the text at --content-max, so 1808 = 224 + 1200 + 384 is
   exactly the width at which every column is at its maximum. Below it
   nothing changes at all; above it the page centres instead of stranding
   the sidebar and the strip at opposite edges of the monitor (measured
   1469px of white between the text and the strip at 3277px).

   The one thing this trades is that the strip is full-bleed to the page
   edge rather than the viewport edge on displays wider than 1808. Revert
   by deleting the two properties. Keep --page-max in step with the three
   column maxima if any of them change. */
/* Site-level, deliberately not in the token layer: assets/flosaic-tokens.css
   is a copy whose canonical source is ../Assets/, and this is a fact about
   this page's three columns, not a brand token. */
:root {
  --page-max: 1808px;
}

.layout,
.footer {
  max-width: var(--page-max);
  margin-inline: auto;
}

.layout {
  display: grid;
  grid-template-columns: clamp(11.5rem, 13.8vw, 14rem) minmax(0, 1fr) clamp(14rem, 24vw, 24rem);
  grid-template-rows: repeat(6, auto);
  min-height: 100vh;
}

.main {
  display: contents;
}

.main > .section {
  grid-column: 2;
  min-width: 0;
}

.sidebar {
  grid-column: 1;
  grid-row: 1 / -1;
}

/* --- 2. Sidebar nav -------------------------------------------------------- */

.sidebar {
  border-right: 1px solid var(--colour-rule);
  padding: var(--space-6) var(--space-5);
}

.sidebar-inner {
  position: sticky;
  top: var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-7);
}

/* The stacked mark is composed from two shipped assets (there is no single
   stacked lockup file; the guidelines p8-p11 construct only the horizontal
   lockups). The spacing below reproduces the Rev2 website mockup, measured
   2026-08-04 from the sidebar of website-mockup-rev2-single-image.png:
     brandmark 79px wide/78 tall, wordmark 85px wide / 19px cap height,
     vertical gap 28px (= 1.47x cap height), wordmark left edge inset 6px
     right of the brandmark's curved edge (optical alignment).
   The mockup sidebar is ~258px vs ours 256px, so sizes carry over 1:1.
   Both SVGs bake clear space into the viewBox (brandmark content inset
   17.6%, bottom 17.4%; wordmark 15.3% left, 32.5% top of its height), and
   the negative margins below cancel it to hit the measured geometry.
   All values are tied to the 7.4rem widths - change them together. */
.sidebar-brand {
  display: block;
}

.sidebar-brandmark {
  width: 7.4rem;
  margin-left: -1.66rem;   /* 17.6% inset + 0.36rem optical overshoot left */
  margin-bottom: -0.72rem; /* baked paddings (2.40rem) minus the 1.68rem gap */
}

.sidebar-logo {
  width: 7.4rem;
  margin-left: -1.13rem;   /* 15.3% of 7.4rem: glyph left edge at 0 */
  margin-bottom: -1rem;    /* most of the wordmark's bottom clear space */
}

/* The horizontal secondary lockup is for the mobile top bar only */
.sidebar-logo-secondary {
  display: none;
  width: 8rem;
}

.site-nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-nav a {
  position: relative;
  display: flex;
  align-items: center;
  padding: var(--space-2) 0;
  min-height: 44px;
  font-weight: var(--weight-body);
  color: var(--colour-text);
  text-decoration: none;
  transition: color var(--motion-duration) var(--motion-ease);
}

/* The active dot hangs into the left margin so labels stay aligned with the
   wordmark's glyph edge and activation never shifts the text */
.site-nav a::before {
  content: "";
  position: absolute;
  left: -1.1em;
  width: 0.55em;
  height: 0.55em;
  border-radius: 50%;
  background: transparent;
  transition: background-color var(--motion-duration) var(--motion-ease);
}

.site-nav a:hover {
  color: var(--colour-link);
}

.site-nav a.is-active {
  font-weight: var(--weight-heading);
}

.site-nav a.is-active::before {
  background: var(--colour-accent);
}

/* --- 3. Hero --------------------------------------------------------------- */

.hero-tagline {
  margin: 0 0 var(--space-5);
  font-size: var(--text-display);
  font-weight: var(--weight-heading);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  max-width: 13ch;
}

/* 38ch, not the 34ch of the first build: the first clause measures 471px and
   34ch gave 433px, so the greedy break landed mid-clause after "every". At
   38ch the clause fits and the next word cannot follow it, so the break falls
   at the comma. The inline-block clauses make that deterministic rather than
   dependent on exact font metrics during webfont swap - each clause moves to
   the next line whole. Reverts to normal wrapping below the breakpoint, where
   no column is wide enough to hold a clause. */
.hero-support {
  font-size: var(--text-lead);
  font-weight: var(--weight-heading);
  line-height: var(--leading-heading);
  max-width: 38ch;
  margin: 0;
}

.hero-support span {
  display: inline-block;
}

.hero .section-content {
  align-self: center;
}

/* --- 4. Section scaffold and photo column ---------------------------------- */

.section {
  border-top: 1px solid var(--colour-rule);
  padding-block: var(--space-8);
  scroll-margin-top: var(--space-4);
}

.section:first-child {
  border-top: 0;
}

.section-inner {
  max-width: var(--content-max);
  padding-inline: var(--page-margin);
}

/* With the photo strip in its own layout column, prose gets the width of the
   main column; ch-based max-widths on the text keep the measure in check. */
.section-content {
  grid-column: 1 / span 10;
}

.section-aside {
  grid-column: 9 / -1;
}

.section-heading {
  margin: 0 0 var(--space-5);
  font-size: var(--text-h2);
  font-weight: var(--weight-display);
  line-height: var(--leading-heading);
}

.section-lead {
  font-size: var(--text-lead);
  font-weight: var(--weight-heading);
  line-height: var(--leading-heading);
  max-width: 40ch;
}

/* Continuous photo strip: the layout's third column, running the full page
   height with images butted top-to-bottom (hairline seams) and full-bleed to
   the right edge, per the early mockup. Crops flex with the page height -
   real images use object-fit: cover - so these slots want texture-type
   photography; the About portrait is the one controlled-crop slot. */
/* Round 2 (Adam, 2026-08-04): more, shorter images at varied heights,
   butted with no seams; the strip spans every section row of the layout
   grid. Two portrait photos overlay it, pinned to the About row - see
   .about-portraits. */
.photo-strip {
  grid-column: 3;
  grid-row: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.photo-strip .figure {
  flex: 2.2 1 0;
  min-height: 5rem;
  display: flex;
}

/* The global .figure + .figure gap must not open seams inside the strip -
   the images butt directly (Adam, round 2/3) */
.photo-strip .figure + .figure {
  margin-top: 0;
}

/* Varied slot heights so the strip reads as a composition, not a repeat.
   Order-coupled to the figures in index.html (round 6: nineteen slots,
   shorter than round 3 per Adam - more photos, each smaller in height).
   The top three slots are Adam's top three picks and run slightly taller.

   THE FLOOR TIER IS SET BY THE 2:1 CROP TOLERANCE AT THE WIDEST LAYOUT,
   not by eye (2026-08-07). The strip column is clamp(14rem, 24vw, 24rem)
   and the text column is capped at --content-max, so both stop growing at
   a 1808px viewport: beyond that the strip is a fixed 384px against a
   fixed ~3920px of page, and that is the flattest every slot ever gets.
   The old tiers 3 / 2.6 / 2.1 / 1.75 put the five floor slots at 176px
   there - a 2.18 ratio, outside tolerance - even though they measured a
   safe 1.95 at 1440. Total strip height is fixed by the page content, so
   lifting the floor has to come out of the taller tiers: the tiers below
   are the flattest-slot-first solve, floor 2.02 landing at 196px / 1.96.
   Changing any value means re-solving the set - the constraint is
   (smallest grow / sum of all grows) * 3920 >= 192. */
/* nth-child counts figures only - the sticky grid panel is a ::before and
   pseudo-elements do not shift nth-child. Round 10 order: 18 slots,
   jetty is slot 10. */
.photo-strip .figure:nth-child(1) { flex-grow: 2.9; min-height: 8rem; } /* top pick */
.photo-strip .figure:nth-child(2) { flex-grow: 2.45; min-height: 6.5rem; }
.photo-strip .figure:nth-child(3) { flex-grow: 2.45; min-height: 6.5rem; }
.photo-strip .figure:nth-child(4) { flex-grow: 2.45; min-height: 6.5rem; }
.photo-strip .figure:nth-child(6) { flex-grow: 2.02; }
.photo-strip .figure:nth-child(8) { flex-grow: 2.02; }
.photo-strip .figure:nth-child(10) { flex-grow: 2.45; min-height: 6.5rem; } /* jetty pano */
.photo-strip .figure:nth-child(12) { flex-grow: 2.02; }
.photo-strip .figure:nth-child(15) { flex-grow: 2.02; }
.photo-strip .figure:nth-child(17) { flex-grow: 2.02; }

.photo-strip .figure-placeholder,
.photo-strip img {
  flex: 1;
  width: 100%;
  aspect-ratio: auto;
  object-fit: cover;
}

/* Ship-size export (2026-08-07) wraps every photo in <picture> - a WebP
   source with a JPEG fallback, both srcset at 400w and 800w. That markup
   is generated: build-images.py owns it, do not hand-edit. display:
   contents dissolves the wrapper so the <img> stays the direct flex or
   grid child and every img rule in this file applies unchanged. */
.photo-strip picture,
.about-portraits picture,
.mobile-strip picture {
  display: contents;
}

/* --- About portrait pair - overlays the strip, pinned to the About row.
   Two photos of Adam, stacked and touching, allowed to push over the
   adjacent strip images (Adam accepted the overlap to guarantee the
   About alignment across widths). Direct child of .main (display:
   contents) so it is a grid item; in DOM order it follows #about, which
   is also its mobile position in flow. */
.about-portraits {
  grid-column: 3;
  grid-row: 5; /* the About section's row - keep in step with .layout rows */
  align-self: center;
  z-index: 1;
  margin: 0;
  /* Push slightly beyond the About row over neighbouring strip images
     rather than letting the row stretch the pair apart */
  margin-block: calc(-1 * var(--space-6));
  /* Thin yellow border ties the pair into the strip grid it sits over
     (Adam, round 4) */
  border: 1px solid var(--yellow);
}

.about-portraits img {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: center top;
}

/* --- Yellow layout grid (Adam, round 3): guidelines p31 idiom, square
   cells, dotted internal lines, solid outer frame. Cell size is a fixed
   module so cells stay square at any width. Sits above the strip images;
   the About portrait pair is a separate grid item with its own z-index,
   so Adam's photos paint over the grid. Audition state on the aside's
   class list - remove "strip-grid" to kill it.

   Round 7 (Adam, 2026-08-05): the grid is now a viewport-pinned sticky
   panel - the photos scroll beneath a static grid. The panel is a flex
   item (::before, so nth-child slot rules are unaffected) one viewport
   tall, negative-margined to zero net height so the figures still fill
   the column; sticky keeps it 10px under the viewport top until the strip
   runs out at the footer, where it slides away naturally. CSS-only: with
   sticky unsupported it degrades to a grid over the top of the strip, and
   prefers-reduced-motion pins it back onto the strip (parallax is motion)
   via the block below. */
.photo-strip {
  position: relative;
}

.strip-grid::before {
  content: "";
  flex: 0 0 auto;
  position: sticky;
  top: 10px;
  height: calc(100vh - 20px);
  margin: 10px;
  pointer-events: none;
  border: 1px solid var(--yellow);
  /* Layers 1-4: small solid triangles pointing inward where each dotted
     internal line meets the solid frame, per the guidelines p31-p32 idiom
     (round 6). Tiles are 88px in the repeat direction with the triangle
     centred at 44.5px; position -45px lands the centres on the gridlines
     at 88k - 0.5, and the -1px edge offsets tuck the 6px-deep bases 1px
     under the frame so they always touch it regardless of subpixel
     rounding (round 7 gap fix). Layer 5: the dotted cell pattern, offset
     so its first row/column sits under the solid frame (round 4).
     Layers 6-8: occasional cells filled solid yellow per guidelines p31 -
     round 9 set (Adam): circle at col 3 row 2, square at col 1 row 7,
     circle at col 2 row 14. Sized 87px to the true cell interior (86px
     left a 1px gap against the right and bottom gridlines - round 7 gap
     fix). Cell (col, row) from the top-left maps to position
     ((col-1)*88, (row-1)*88)px. The row-14 circle needs a viewport at
     least ~1250px tall to show at all - deliberate, Adam's placement on
     his own display. Move or add fills by editing these positions
     (tuning knobs, Adam's eye). */
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='6'%3E%3Cpath d='M40.5 0h8L44.5 6z' fill='%23F1E744'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='6'%3E%3Cpath d='M40.5 6h8L44.5 0z' fill='%23F1E744'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='88'%3E%3Cpath d='M0 40.5v8L6 44.5z' fill='%23F1E744'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='88'%3E%3Cpath d='M6 40.5v8L0 44.5z' fill='%23F1E744'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='88'%3E%3Cpath d='M0 .5h88M.5 0v88' fill='none' stroke='%23F1E744' stroke-width='1' stroke-dasharray='2 4'/%3E%3C/svg%3E"),
    radial-gradient(circle closest-side, var(--yellow) 98%, transparent 100%),
    linear-gradient(var(--yellow), var(--yellow)),
    radial-gradient(circle closest-side, var(--yellow) 98%, transparent 100%);
  background-size: 88px 6px, 88px 6px, 6px 88px, 6px 88px, 88px 88px, 87px 87px, 87px 87px, 87px 87px;
  background-position: left -45px top -1px, left -45px bottom -1px, left -1px top -45px, right -1px top -45px, -1px -1px, 176px 88px, 0px 528px, 88px 1144px;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y, repeat, no-repeat, no-repeat, no-repeat;
}

/* The repeating edge-triangle layers put a half triangle wherever the
   hidden k=0 gridline meets a frame corner. The mask below cuts a ~7px
   notch inside each corner while a 1px ring keeps the frame itself
   intact; interior triangles and fills sit beyond 7px from any edge, so
   only the corner artefacts go (Adam, round 8). Unprefixed mask is fine
   in every current browser; re-check at the stage 6 cross-browser pass. */
.strip-grid::before,
.mobile-strip::after {
  mask-image:
    linear-gradient(180deg, transparent 7px, #000 7px, #000 calc(100% - 7px), transparent calc(100% - 7px)),
    linear-gradient(90deg, transparent 7px, #000 7px, #000 calc(100% - 7px), transparent calc(100% - 7px)),
    linear-gradient(180deg, #000 1px, transparent 1px),
    linear-gradient(0deg, #000 1px, transparent 1px),
    linear-gradient(90deg, #000 1px, transparent 1px),
    linear-gradient(270deg, #000 1px, transparent 1px);
}

/* The panel's viewport-height of column space is cancelled by pulling the
   first photo up underneath it, NOT by a negative margin on the panel
   itself: the sticky clamp bounds the panel's margin box, so a huge
   negative bottom margin would let its visible box overshoot the strip
   bottom and hang over the footer. With margin 10px all round, the clamp
   also lands the panel exactly 10px off the strip bottom at full scroll. */
.strip-grid .figure:first-of-type {
  margin-top: -100vh;
}

/* A viewport-pinned backdrop while content scrolls is a motion effect:
   under reduced motion the grid reverts to painting over the full strip
   and scrolling with it (the round 3-6 behaviour). */
@media (prefers-reduced-motion: reduce) {
  .strip-grid::before {
    position: absolute;
    inset: 10px;
    height: auto;
    margin: 0;
  }

  .strip-grid .figure:first-of-type {
    margin-top: 0;
  }
}

/* Photo-column figure. Until the stage 5 photography pass, every image slot
   carries an honest placeholder: striped, labelled, dimensions stated. */
.figure {
  margin: 0;
}

.figure + .figure {
  margin-top: var(--space-4);
}

/* Mobile-persistence figures (round 2): hidden wherever the strip carries
   the imagery, shown by the 768px media block */
.mobile-figure {
  display: none;
}

/* Mobile thin photo row (round 6, Adam 2026-08-05): replaces the single hero
   mobile figure with a row of the top three picks, carrying the yellow grid
   so the gridline motif survives narrow widths (his 3A ask). Hidden wherever
   the desktop strip carries the imagery; shown by the 768px media block. */
.mobile-strip {
  display: none;
}

.figure-placeholder {
  aspect-ratio: 4 / 5;
  border: 1px solid var(--colour-rule);
  background: repeating-linear-gradient(
    45deg,
    var(--grey-5),
    var(--grey-5) 12px,
    var(--colour-surface) 12px,
    var(--colour-surface) 24px
  );
  display: grid;
  place-items: center;
}

.figure-placeholder-label {
  font-family: Consolas, "Courier New", monospace;
  font-size: var(--text-small);
  color: var(--colour-text-muted);
  background: var(--colour-surface);
  padding: var(--space-1) var(--space-2);
  text-align: center;
}

.figure-caption {
  margin-top: var(--space-2);
  font-size: var(--text-small);
  color: var(--colour-text-muted);
}

/* --- 5. Services ----------------------------------------------------------- */

.service {
  max-width: 58ch;
}

.service + .service {
  margin-top: var(--space-6);
}

.service h3 {
  margin: 0 0 var(--space-2);
  font-size: var(--text-h3);
  font-weight: var(--weight-heading);
  line-height: var(--leading-heading);
}

.service p {
  margin-bottom: 0;
}

/* --- 6. Evidence band (stat tiles) ----------------------------------------- */

/* Feature band: the page's alternate-background moment, used twice as a
   deliberate pattern (evidence band after the hero, approach strip after
   Services - Adam, 2026-08-04). */
.band {
  background: var(--colour-surface-alt);
  border-top: 1px solid var(--colour-rule);
  padding-block: var(--space-7);
}

.evidence-lead {
  grid-column: 1 / -1;
  font-size: var(--text-lead);
  font-weight: var(--weight-heading);
  line-height: var(--leading-heading);
  max-width: 56ch;
  margin: 0 0 var(--space-6);
}

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

/* Data figure pattern from the report scale: label (Medium) above the big
   number (Extra Light), caption (Light) below. */
.stat {
  border-top: 3px solid var(--colour-accent);
  padding-top: var(--space-3);
}

/* Labels reserve two lines so the big figures share a baseline across the row.
   "detailed site assessments" fits on one line where the two savings labels
   wrap to two, and without this the first figure rides 1 line higher than the
   others. Fallback first for browsers without the lh unit; the tiles stack
   below the breakpoint, where the reservation is reset to avoid dead space. */
.stat-label {
  font-weight: var(--weight-heading);
  font-size: var(--text-body);
  margin: 0 0 var(--space-2);
  min-height: 3.1em;
  min-height: 2lh;
}

.stat-figure {
  font-size: var(--text-stat);
  font-weight: var(--weight-display);
  line-height: var(--leading-tight);
  margin: 0 0 var(--space-2);
}

.stat-caption {
  font-size: var(--text-small);
  color: var(--colour-text-muted);
  margin: 0;
  max-width: 26ch;
}

/* --- 7. Approach strip (line-element idiom, guidelines p27-p29) ------------ */

.approach-list {
  list-style: none;
  margin: 0;
  padding: 0;
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--grid-gutter);
}

.approach-list li {
  border-top: 3px solid var(--colour-accent);
  padding-top: var(--space-3);
}

.approach-list b {
  display: block;
  font-weight: var(--weight-heading);
  margin-bottom: var(--space-2);
}

/* --- 8. About -------------------------------------------------------------- */

.about-prose {
  max-width: 58ch;
}

/* --- 9. Contact ------------------------------------------------------------ */

/* The one Extra Light display moment on the page - the mockup's dropped
   "Enquire." layer, relocated here as the contact heading. */
.contact-heading {
  margin: 0 0 var(--space-5);
  font-size: var(--text-display);
  font-weight: var(--weight-display);
  line-height: var(--leading-tight);
}

.contact-email {
  font-size: var(--text-h3);
  font-weight: var(--weight-heading);
}

.contact-details {
  margin: var(--space-4) 0 0;
}

.contact-details p {
  margin-bottom: var(--space-2);
}

/* --- 9a. Enquiry form ------------------------------------------------------ */

/* Added stage 7, 2026-08-08. The page's only interactive controls.
   Form elements do not inherit typography from body the way ordinary elements
   do, so `font: inherit` is set on every control - without it they fall back
   to the UA font and the ss05 tailed `l` silently stops rendering inside the
   fields while the rest of the page keeps it. */

.enquiry-form {
  margin: var(--space-6) 0 0;
  max-width: 34rem;
}

.enquiry-form .field {
  margin: 0 0 var(--space-4);
}

.enquiry-form label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--text-small);
  font-weight: var(--weight-heading);
}

.enquiry-form .optional {
  font-weight: var(--weight-body);
  color: var(--colour-text-muted);
}

.enquiry-form input[type="text"],
.enquiry-form input[type="email"],
.enquiry-form textarea {
  font: inherit;
  display: block;
  box-sizing: border-box;
  width: 100%;
  padding: var(--space-2) var(--space-3);
  color: var(--colour-text);
  background: var(--colour-surface);
  border: 1px solid var(--colour-rule);
  border-radius: 0;      /* square, like the stat tiles and the grid overlay */
  -webkit-appearance: none;
  appearance: none;
}

.enquiry-form textarea {
  min-height: 8rem;
  resize: vertical;
}

.enquiry-form input:focus-visible,
.enquiry-form textarea:focus-visible {
  outline: 2px solid var(--colour-link);
  outline-offset: 2px;
  border-color: var(--colour-link);
}

/* The site's first button. Yellow is barred as a background for text
   (1.29:1), so the fill is charcoal with white type at 15.13:1. Square and
   flat, so it reads as a deliberate object rather than a tinted link. */
.enquiry-form button {
  font: inherit;
  font-weight: var(--weight-heading);
  padding: var(--space-3) var(--space-5);
  color: var(--white);
  background: var(--charcoal);
  border: 1px solid var(--charcoal);
  border-radius: 0;
  cursor: pointer;
  transition: background var(--motion-duration) var(--motion-ease);
}

.enquiry-form button:hover {
  background: var(--grey-80);   /* white on #515151 is 7.94:1 */
  border-color: var(--grey-80);
}

.enquiry-form button:focus-visible {
  outline: 2px solid var(--colour-link);
  outline-offset: 2px;
}

.enquiry-form-note {
  margin: var(--space-4) 0 0;
  font-size: var(--text-small);
  color: var(--colour-text-muted);
}

/* Honeypot. Off-canvas rather than display:none - some bots skip inputs that
   are display:none, which defeats the point of the trap. */
.enquiry-form-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* --- 10. Footer ------------------------------------------------------------ */

.footer {
  border-top: 1px solid var(--colour-rule);
  padding: var(--space-5) var(--page-margin);
  font-size: var(--text-small);
  color: var(--colour-text-muted);
}

.footer p {
  margin: 0;
}

/* --- Annotate markers (guidelines p27) - audition states, 2026-08-05 -------
   Hand-drawn-style hairline curves with yellow dot terminals, per Adam's
   round 7 ask: curved arrow at the stat row, floating curve-and-dot near
   the Enquire heading, looping spring beside the services text. Ink is
   charcoal (round 12): the p27 markers measure #262626 in the guidelines
   PDF's vector strokes - the earlier grey-40 was a softening, not the
   brand idiom. Each svg in index.html is one element to delete; all hide
   under 1024px where their whitespace vanishes. Positions are tuning
   knobs for Adam's eye. */
/* No CSS blur here: the round-13 whole-svg blur is superseded by the
   round-14 in-svg ink profile - each line is a blurred halo pass plus a
   crisp core at ~62% width, so the stroke cross-section peaks dark in
   the centre and fades at the edges while the yellow dots stay crisp.
   Knobs (halo sigma, core scale) live in
   Brand-Rollout/annot-marker-gen.py. */
.annot {
  position: absolute;
  pointer-events: none;
  color: var(--charcoal);
}

.annot .dot {
  fill: var(--yellow);
}

.stat-row {
  position: relative;
}

/* Rises from the band whitespace beneath the tiles, tail fading off to
   the lower right (Adam, rounds 8-9). Round 11: the tip (x=30 in the
   150px viewBox) sits on the horizontal centre of the middle tile's
   yellow rule - the middle column's centre is exactly 50% of the row. */
.annot-stat {
  width: 150px;
  top: calc(100% + 2px);
  left: calc(50% - 30px);
}

#services {
  position: relative;
}

/* Centred in the whitespace between the services text and the photo strip
   (Adam, round 11): anchored to #services so right: 0 is the strip
   boundary, and the auto margins centre the fixed width between that and
   the text's right edge. The text edge is min(58ch, the 10-column content
   width) - the column is narrower than 58ch below ~1430px and the
   max-width bites above it; the second min() operand is the 10-of-12
   column span written out against the section width (grid = 100% minus
   both page margins; 10 columns + 9 gutters reduces to 5/6 grid minus a
   sixth of one gutter). Sits lower in the section than rounds 7-10 did.
   Below ~1240px the whitespace vanishes; its own hide rule sits with the
   shared one at the 1024px block. */
.annot-spring {
  width: 96px;
  top: 45%;
  left: calc(var(--page-margin) + min(58ch, (100% - 2 * var(--page-margin)) * 5 / 6 - var(--grid-gutter) / 6));
  right: 0;
  margin-inline: auto;
}

@media (max-width: 1240px) {
  .annot-spring {
    display: none;
  }
}

/* Zero-size inline anchor after the heading text (round 12): an empty
   inline-block's baseline is its bottom edge, so bottom: 0 on the abs
   child is the text baseline, and left: 0 is the text end - the marker
   tracks the word at any heading size without being part of it. */
.annot-anchor {
  position: relative;
  display: inline-block;
  width: 0;
  height: 0;
}

/* Free-floating marker (round 12): the pre-round-11 sweep curving down
   into the yellow dot terminal, hung roughly halfway between the end of
   "Enquire" and the photo strip - Adam's ask, moving it clear of the
   word; the heading itself no longer carries a full stop. The halfway
   offset from the word end: the strip edge sits at 76vw (100vw minus the
   24vw strip column, scrollbar ignored) and the word end is ~536px from
   the viewport left wherever the display clamp is maxed (above ~1209px),
   so halfway = 38vw - 268px, minus the dot's 12-unit inset in the
   viewBox. "Roughly" is the brief - a tuning knob, and it degrades
   gracefully in the 1024-1209 clamp band. The dot sits 8 viewBox units
   up from the svg base, about where a full stop's centre would ride; it
   paints last inside the svg, so the line ends behind it. */
/* Two branches, because the strip column and the sidebar stop growing
   before the page does. The word end sits at sidebar + --page-margin +
   ~292px (the word's own width above the display clamp), so it is NOT a
   fixed 536px from the page's left edge as the round 12 note assumed -
   the sidebar is 13.8vw until it caps at 14rem around 1623px, which is
   what makes the two branches differ.
     below 1600px  strip is 24vw, sidebar 13.8vw -> 0.38vw - 280
     1628px and up strip is 384, sidebar 224, page caps at --page-max
                   -> (pageWidth - 384 - 224 - 292) / 2, less the dot's
                      12-unit inset in the viewBox = pageWidth/2 - 486
   max() picks whichever branch is live and keeps the transition
   continuous. Measured against true halfway: exact at and above 1628px,
   +1.5px at 1440, -6px at 1280 - all well inside the "roughly halfway"
   brief, in a gap ~860px wide. */
.annot-enquire {
  width: 118px;
  left: max(min(100vw, 1600px) * 0.38 - 280px,
            min(100vw, var(--page-max)) / 2 - 486px);
  bottom: 0;
}

@media (max-width: 1024px) {
  .annot {
    display: none;
  }
}

/* --- Responsive ------------------------------------------------------------ */

@media (max-width: 1024px) {
  .layout {
    grid-template-columns: 10.5rem minmax(0, 1fr) clamp(11rem, 20vw, 14rem);
  }

  .sidebar {
    padding-inline: var(--space-4);
  }

  .section-content {
    grid-column: 1 / -1;
  }
}

@media (max-width: 768px) {
  /* Sidebar collapses to a sticky top bar: horizontal secondary lockup,
     nav in a row. The stacked-header variant is prototyped separately in
     components/nav-sidebar.html for the stage 4 audition. */
  .layout {
    display: block;
  }

  .sidebar {
    position: sticky;
    top: 0;
    z-index: 10;
    background: var(--colour-surface);
    border-right: 0;
    border-bottom: 1px solid var(--colour-rule);
    padding: var(--space-2) var(--page-margin);
  }

  .sidebar-inner {
    position: static;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
  }

  .sidebar-brandmark,
  .sidebar-logo {
    display: none;
  }

  .sidebar-brand {
    flex-shrink: 0;
  }

  .sidebar-logo-secondary {
    display: block;
    width: clamp(6.5rem, 24vw, 8rem);
  }

  .site-nav ul {
    display: flex;
    gap: var(--space-3);
  }

  .site-nav a {
    padding: var(--space-2);
  }

  /* The dot slot reads as clutter in a horizontal row; active weight and
     an underline carry the state instead. */
  .site-nav a::before {
    display: none;
  }

  .site-nav a.is-active {
    text-decoration: underline;
    text-decoration-color: var(--colour-accent);
    text-decoration-thickness: 3px;
    text-underline-offset: 6px;
  }

  .section {
    scroll-margin-top: 4.5rem;
  }

  .grid {
    display: block;
  }

  .section-aside {
    margin-top: var(--space-5);
  }

  /* The continuous strip has no meaningful single-column equivalent. Round 2
     mobile treatment (Adam, 2026-08-04): the portrait pair and a few key
     images persist inline; the rest of the strip hides. */
  .photo-strip {
    display: none;
  }

  /* Portrait pair flows after the About section (its DOM position): side by
     side, touching, full width */
  .about-portraits {
    display: flex;
    margin-block: 0;
  }

  .about-portraits img {
    width: 50%;
  }

  .mobile-figure {
    display: block;
    margin: var(--space-5) 0 0;
  }

  .mobile-figure img {
    width: 100%;
    aspect-ratio: 3 / 2;
    object-fit: cover;
  }

  /* Thin photo row under the hero copy: three images butted, grid overlaid.
     Same idiom as the desktop strip-grid (dotted 88px cells, solid frame,
     inward triangles at each line-frame junction) minus the cell fills,
     which have no room in a single-cell-tall band. */
  .mobile-strip {
    display: flex;
    gap: 0;
    position: relative;
    margin-top: var(--space-5);
  }

  .mobile-strip img {
    width: 33.333%;
    height: 14rem; /* doubled from 7rem - Adam, round 8 */
    object-fit: cover;
  }

  .mobile-strip::after {
    content: "";
    position: absolute;
    inset: 6px;
    pointer-events: none;
    border: 1px solid var(--yellow);
    background-image:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='6'%3E%3Cpath d='M40.5 0h8L44.5 6z' fill='%23F1E744'/%3E%3C/svg%3E"),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='6'%3E%3Cpath d='M40.5 6h8L44.5 0z' fill='%23F1E744'/%3E%3C/svg%3E"),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='88'%3E%3Cpath d='M0 40.5v8L6 44.5z' fill='%23F1E744'/%3E%3C/svg%3E"),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='88'%3E%3Cpath d='M6 40.5v8L0 44.5z' fill='%23F1E744'/%3E%3C/svg%3E"),
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='88'%3E%3Cpath d='M0 .5h88M.5 0v88' fill='none' stroke='%23F1E744' stroke-width='1' stroke-dasharray='2 4'/%3E%3C/svg%3E");
    background-size: 88px 6px, 88px 6px, 6px 88px, 6px 88px, 88px 88px;
    background-position: left -45px top -1px, left -45px bottom -1px, left -1px top -45px, right -1px top -45px, -1px -1px;
    background-repeat: repeat-x, repeat-x, repeat-y, repeat-y, repeat;
  }

  .figure-placeholder {
    aspect-ratio: 3 / 2;
  }

  .stat-row,
  .approach-list {
    display: block;
  }

  /* Stacked tiles have no row to align to - drop the two-line reservation. */
  .stat-label {
    min-height: 0;
  }

  /* No column here is wide enough to hold a hero clause whole, so let the
     support line wrap normally rather than stranding a short second line. */
  .hero-support span {
    display: inline;
  }

  .stat + .stat,
  .approach-list li + li {
    margin-top: var(--space-5);
  }

  .stat-row {
    margin-top: 0;
  }
}

@media (max-width: 420px) {
  .sidebar-inner {
    gap: var(--space-2);
  }

  .sidebar-logo-secondary {
    width: 6rem;
  }

  .site-nav ul {
    gap: var(--space-2);
  }

  .site-nav a {
    font-size: 0.9375rem;
    padding: var(--space-2) var(--space-1);
  }
}
