/* ==========================================================================
   Blessit George — site stylesheet
   Epic 1: Shared Design System & Shell

   CONTENTS
     1. TOKENS ................ US-006  <- change colours, fonts, spacing here
     2. FONT FACES ............ US-004
     3. RESET & BASE .......... US-014
     4. LAYOUT SHELL .......... US-014
     5. TYPOGRAPHY ............ US-007
     6. CTA BUTTON ............ US-008
     7. STICKY NAV ............ US-009
     8. FOOTER ................ US-010
     9. EXPANDER .............. US-011
    10. MOTION ................ US-012

   Nothing below section 1 contains a hard-coded hex value (US-006).
   ========================================================================== */


/* ==========================================================================
   1. TOKENS — US-006
   ========================================================================== */

/* --------------------------------------------------------------------------
   FONT FAMILIES — the single place to swap a typeface.

   To change a font you edit TWO things and nothing else:
     a) the two variables below, and
     b) the matching @font-face block in section 2.
   No other rule in this stylesheet names a font family.
   -------------------------------------------------------------------------- */
:root {
  --font-heading: "Merriweather", Georgia, "Times New Roman", serif;
  --font-body:    "Raleway", -apple-system, BlinkMacSystemFont, "Segoe UI",
                  Helvetica, Arial, sans-serif;

  /* Weights are tokens too, so a swapped family can re-map them in one place. */
  --weight-heading: 700;
  --weight-body:    400;
  --weight-strong:  600;
}

/* --------------------------------------------------------------------------
   COLOUR — LIGHT THEME (default; the founders page `/` never leaves it)
   Every pair verified against WCAG 2.1 AA by scripts/contrast.py.
   -------------------------------------------------------------------------- */
:root {
  --ground:        #FFFFFF;   /* page background                              */
  --surface:       #F9F7F4;   /* filled card (US-020 tier 3)   ink 13.42:1    */
  --ink:           #2A2A2A;   /* body and heading text         14.35:1        */
  --muted:         #66717F;   /* secondary text                 4.96:1        */
  --rule:          #E2DCD2;   /* hairlines and card borders (non-text)        */
  --rule-section:  #C4A97C;   /* section divider — accent at rule weight      */

  --accent:        #8A6A3B;   /* CTA fill / links               4.99:1        */
  --accent-hover:  #534023;   /* hover:   accent −40% (US-008 as amended D38) */
  --accent-active: #1C150C;   /* pressed: accent −80% (US-008 as amended D38) */
  --on-accent:     #FFFFFF;   /* CTA label in light theme       4.99:1        */

  --focus:         #2A2A2A;   /* focus ring, drawn offset onto the ground     */

  /* The sticky bar, D55. Its RGB is --ground's, restated because the alpha
     has to be baked in: color-mix() would express this as one value derived
     from --ground, but it needs Safari 16.2 and US-060's baseline is iOS 15.
     If --ground ever changes, change this with it. */

  /* ---- Semantic scale, D60 -------------------------------------------
     Added for the work samples, which carry meaning in colour that a
     single accent cannot express: MoSCoW priority, pass/fail coverage,
     Gherkin keywords. Available to the rest of the site.

     Solved rather than picked: each is the most saturated value that still
     clears 5.0:1 on --ground, and each -soft is the strongest tint that
     keeps --ink at roughly 11:1. The 0.5 over AA is deliberate headroom —
     a scale sitting exactly on 4.50 breaks the moment anything shifts.

     Hues are spaced so must/could stay separable under deuteranopia, which
     red-vs-green alone would not be.                                      */
  --sem-must:        #C14633;   /* 5.01:1 — MoSCoW Must, failures     */
  --sem-must-soft:   #F2DCD9;
  --sem-should:      #8F6826;   /* 5.03:1 — Should, warnings          */
  --sem-should-soft: #EDDFC8;
  --sem-could:       #3171B9;   /* 5.01:1 — Could, informational      */
  --sem-could-soft:  #D5E2F1;
  --sem-good:        #217D5C;   /* 5.06:1 — covered, passing          */
  --sem-good-soft:   #C1EADB;
  --nav-bg:        rgb(255 255 255 / 0.80);
  --nav-shadow:    0 1px 2px rgb(0 0 0 / 0.05), 0 6px 20px rgb(0 0 0 / 0.08);
}

/* --------------------------------------------------------------------------
   COLOUR — DARK THEME (US-013; applied only by /agencies)

   Opt-in via <html data-theme="dark">. Deliberately NOT wired to
   prefers-color-scheme: US-013 requires `/` to stay light under any system
   preference, so the theme is a property of the route, not of the visitor.
   -------------------------------------------------------------------------- */
[data-theme="dark"] {
  --ground:        #151719;
  --surface:       #1E2124;   /* ink 13.91:1                                  */
  --ink:           #ECEEF0;   /* 15.45:1                                      */
  --muted:         #A8B2BE;   /*  8.37:1                                      */
  --rule:          #332E28;
  --rule-section:  #6B5636;

  --accent:        #D8AC68;   /*  8.58:1                                      */
  --accent-hover:  #E8CDA4;   /* hover:   +40% (dark brightens — see below)   */
  --accent-active: #F7EEE1;   /* pressed: +80%. Dark BRIGHTENS because
                                 darkening moves the fill toward its own
                                 dark-ink label: −40% is 3.39:1, −80% is
                                 1.15:1. Both fail. See D38.               */
  --on-accent:     #151719;   /* dark ink, NOT white — white measures 2.10:1
                                 and is prohibited by US-013                  */

  --focus:         #ECEEF0;

  /* --ground's RGB at the same alpha. The shadow is heavier: on a dark ground
     a 0.08 black shadow is invisible, so the separation has to come from a
     denser one. */

  /* Semantic scale, dark. Same solve against the dark ground. D60. */
  --sem-must:        #D26857;   /* 5.01:1 */
  --sem-must-soft:   #462C27;
  --sem-should:      #AD7F2E;   /* 5.01:1 */
  --sem-should-soft: #3B3121;
  --sem-could:       #4D8AD0;   /* 5.02:1 */
  --sem-could-soft:  #263443;
  --sem-good:        #299970;   /* 5.04:1 */
  --sem-good-soft:   #1F372E;
  --nav-bg:        rgb(21 23 25 / 0.80);
  --nav-shadow:    0 1px 2px rgb(0 0 0 / 0.35), 0 6px 20px rgb(0 0 0 / 0.45);
}

/* --------------------------------------------------------------------------
   TYPE SCALE — US-007
   Fluid between the two viewports the acceptance criteria pin:
     375px -> body 17 / h1 30 / h2 22 / h3 18
    1280px -> body 17 / h1 46 / h2 28 / h3 20
   Expressed in rem so browser font-size settings still scale the page.
   -------------------------------------------------------------------------- */
:root {
  --text-body: 1.0625rem;                                       /* 17px       */
  --text-h1:   clamp(1.875rem, 1.4606rem + 1.7680vw, 2.875rem);  /* 30 -> 46  */
  --text-h2:   clamp(1.375rem, 1.2196rem + 0.6630vw, 1.75rem);   /* 22 -> 28  */
  --text-h3:   clamp(1.125rem, 1.0732rem + 0.2210vw, 1.25rem);   /* 18 -> 20  */
  --text-h2-tight: clamp(1.25rem, 1.2003rem + 0.2210vw, 1.375rem); /* 20 -> 22 */
  --text-small: 0.9375rem;                                       /* 15px      */

  --leading-body:    1.7;
  --leading-heading: 1.2;
  --tracking-heading: -0.01em;
}

/* --------------------------------------------------------------------------
   SPACE, SHAPE, LAYOUT — US-006, US-014
   -------------------------------------------------------------------------- */
:root {
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.25rem;   /* 20px — mobile side padding (US-006) */
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4.5rem;
  --space-9: 6rem;

  --radius: 4px;              /* US-006 */
  --measure: 42.5rem;         /* 680px reading measure (US-006) */
  --shell: 54.5rem;           /* 872px outer container — every section */
  --gutter: var(--space-5);   /* 20px mobile side padding (US-006) */
  --nav-height: 3.5rem;
  --tap-target: 44px;         /* iOS minimum, used by nav and expander */

  --transition: 160ms ease;
}

@media (min-width: 48em) {   /* 768px */
  :root { --gutter: var(--space-6); }
}


/* ==========================================================================
   2. FONT FACES — US-004

   Self-hosted, Latin-subset woff2, served from this origin. No request is
   made to fonts.googleapis.com or fonts.gstatic.com — that is what lets the
   site run without a consent banner (NFR-7).

   font-display: swap keeps text visible in the fallback stack while the
   files load, and renders the fallback permanently if a file 404s (EH-5).
   ========================================================================== */

@font-face {
  font-family: "Merriweather";
  src: url("../fonts/merriweather-latin-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Raleway";
  src: url("../fonts/raleway-latin-variable.woff2") format("woff2");
  font-weight: 400 600;     /* variable: one file covers body and strong */
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


/* ==========================================================================
   3. RESET & BASE — US-014
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Keeps anchor targets clear of the sticky bar (US-009 + US-017). The one
     and only clearance rule — see .section, which used to duplicate it. */
  scroll-padding-top: calc(var(--nav-height) + var(--space-4));
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: var(--weight-body);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  /* US-014: no horizontal scrollbar at any width from 320px to 1920px. */
  overflow-x: hidden;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

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

a { color: var(--accent); text-underline-offset: 0.2em; }
a:hover { color: var(--accent-hover); }

strong, b { font-weight: var(--weight-strong); }

hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: var(--space-8) 0;
}

/* Shared focus treatment. The 2px ring is drawn OFFSET, so it sits on the
   page ground rather than on the component — which is why --focus only has
   to clear 3:1 against --ground (US-008). */
:where(a, button, summary, [tabindex]):focus {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius);
}
/* Progressive enhancement: browsers that understand :focus-visible drop the
   ring for pointer input. iOS Safari 15.0-15.3 does not, and correctly keeps
   the always-on :focus ring above (US-060 baseline). */
:where(a, button, summary, [tabindex]):focus:not(:focus-visible) {
  outline: none;
}
:where(a, button, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Scroll-to-top target for the nav name (D73). It needs a real layout box:
   an empty INLINE element generates no line box at all, so there is nothing
   for the browser to scroll to — verified, the link updated the hash and the
   page stayed exactly where it was. Block with zero height gives it a box at
   document position 0 while occupying no space.

   It also cannot live on .site-header, which is position:sticky and therefore
   already in view at every scroll offset, making the scroll a no-op. Both
   failures looked identical from the outside: hash changes, nothing moves. */
#top {
  display: block;
  height: 0;
}

/* Keyboard-only skip link (US-062 groundwork). */
.skip-link {
  position: absolute;
  left: var(--gutter);
  top: var(--space-2);
  z-index: 100;
  padding: var(--space-2) var(--space-4);
  background: var(--ground);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  transform: translateY(-200%);
  transition: transform var(--transition);
}
.skip-link:focus { transform: translateY(0); }

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


/* ==========================================================================
   4. LAYOUT SHELL — US-014
   ========================================================================== */

/* One container width across the whole page. --shell is the outer frame;
   the 680px reading measure is enforced on the text itself (see `p` in
   section 5), not by narrowing the container. That keeps every section
   edge-aligned while body copy still stops at a comfortable line length. */
.wrap {
  width: 100%;
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--gutter);  /* 20px on mobile (US-006) */
}

/* Retained as a no-op alias so existing markup keeps working. */
.wrap--wide { max-width: var(--shell); }

/* Opt back in to the narrow reading column where a section is pure prose. */
.wrap--measure { max-width: var(--measure); }

/* scroll-margin-top removed. html already carries scroll-padding-top at the
   same calc, and the two STACK — every anchor landed 144px below the viewport
   top under a 56px bar, which is more than twice the clearance either rule was
   asking for. Two mechanisms for one job, each unaware of the other.

   scroll-padding-top on the scroll container is the one to keep: it governs
   every fragment landing on the page, including #top, rather than only the
   elements that happen to carry the class. Anchors now land at 72px — the bar
   plus one --space-4 of air, which is what the original calc meant. D79. */
.section {
  padding-block: var(--space-8);
}

/* 2px and accent-derived, not 1px cool grey. The old hairline was a blue-grey
   sitting in a palette that is warm everywhere else — a small discord repeated
   seven times down the page, and part of why the page read as generic once
   D66 removed the buttons that had been carrying the colour.

   --rule-section is the accent lightened to divider weight rather than the
   accent itself. Seven full-width lines at full --accent strength compete with
   the button they are supposed to lead the eye toward; at this weight the
   rhythm reads as deliberate and the CTA stays the loudest thing on screen.
   D68. */
.section + .section { border-top: 2px solid var(--rule-section); }

.stack > * + * { margin-top: var(--space-4); }
.stack--tight > * + * { margin-top: var(--space-2); }
.stack--loose > * + * { margin-top: var(--space-6); }


/* ==========================================================================
   5. TYPOGRAPHY — US-007
   ========================================================================== */

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-heading);
  margin: 0;
  /* US-007: the mobile hero headline must not overflow or clip a word. */
  overflow-wrap: break-word;
  text-wrap: balance;         /* progressive; ignored where unsupported */
}

h1 { font-size: var(--text-h1); }
h2 { font-size: var(--text-h2); }
h3 { font-size: var(--text-h3); }

/* Body copy fills its container rather than stopping at the 680px reading
   measure. Owner's decision, D49: text stopping short of the images and the
   section edge read as an unfinished layout. This knowingly breaks the
   "measure does not exceed 680px" scenario in US-006 and US-014 — at the
   872px shell a line runs about 100 characters. --measure is retained as a
   token and is still available via .wrap--measure. */
p { margin: 0; }

.lede {
  font-size: var(--text-body);
  color: var(--muted);
}

.small { font-size: var(--text-small); }
.muted { color: var(--muted); }


/* ==========================================================================
   6. CTA BUTTON — US-008
   Label is always exactly "Book a call". Four states: rest, hover, active,
   focus. Implemented as an anchor so it works with JavaScript disabled and
   degrades to an ordinary link (US-040).
   ========================================================================== */

.cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-target);
  padding: var(--space-3) var(--space-6);
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--font-body);
  font-weight: var(--weight-strong);
  font-size: var(--text-body);
  line-height: 1;
  text-decoration: none;
  border: 0;
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color var(--transition);
}

.cta:hover  { background: var(--accent-hover);  color: var(--on-accent); }
.cta:active { background: var(--accent-active); color: var(--on-accent); }

.cta--block { display: flex; width: 100%; }

/* Compact variant for the sticky bar only. Keeps the 44px tap target. */
.cta--compact {
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-small);
}


/* ==========================================================================
   7. STICKY NAV — US-009
   Desktop (>=768px): five anchors + CTA.
   Mobile  (<768px):  CTA only, single row, no wrap.
   ========================================================================== */

/* The sticky element must be the OUTERMOST one. A sticky child can only
   travel within its parent's content box — and a <header> that wraps only
   the nav is exactly as tall as the nav, giving zero travel. The bar then
   scrolls away on the first pixel, which is the bug this replaced. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
}

/* The bar separates itself from the page with a cast shadow rather than a
   hairline, and lets the page show faintly through it. D55.

   No JavaScript, and no scroll listener. The translucency is permanent — at
   the top of the page the only thing behind the bar is --ground, so it reads
   as solid; the effect appears by itself as content passes underneath. A
   scroll-triggered version would have needed a script on BOTH pages, and
   D53 kept /agencies script-free on purpose.

   CSS scroll-driven animation would have done it without script, but
   animation-timeline needs Safari 26 and US-060's floor is iOS Safari 15. */
.nav {
  min-height: var(--nav-height);
  background: var(--ground);
  box-shadow: var(--nav-shadow);
}

/* Translucency only where the blur behind it works. Without the blur, body
   text scrolling under a flat 80% bar stays sharp and legible through it,
   which is unreadable — so browsers lacking backdrop-filter keep the solid
   bar above. Progressive enhancement: the fallback is the better-looking
   failure, not a broken one. */
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
  .nav {
    background: var(--nav-bg);
    -webkit-backdrop-filter: saturate(180%) blur(12px);
    backdrop-filter: saturate(180%) blur(12px);
  }
}

/* Mobile: the bar is the anchor row and nothing else. The name and the CTA
   are removed — the CTA becomes a floating button that gets out of the way
   when a real one is on screen (D46). Desktop keeps all three inline. */
.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: var(--nav-height);
  flex-wrap: nowrap;
}

.nav__name,
.nav .cta { display: none; }

/* The agencies header has no anchor set, so the rule above would empty it
   entirely on mobile. Here the name and the button ARE the bar at every
   width — which also means this page needs no floating CTA and no JavaScript,
   since the header button never leaves the screen. US-029. */
.nav--minimal .nav__name,
.nav--minimal .cta { display: inline-flex; }

.nav--minimal .nav__name { align-items: center; }

.nav__links {
  display: flex;
  align-items: center;
  /* space-4 on mobile, space-5 from 768px. The Services chevron costs 10px and
     the row only ever had 4px spare, so the gap gives the difference back:
     four gaps at 16px rather than 20px frees 16px and the row fits again.
     Desktop has 261px of slack and keeps the roomier gap. D61. */
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;

  /* Mobile: the whole bar, scrolling sideways if the labels overflow. */
  flex: 1 1 100%;

  /* The row is 325px of labels in a 335px container, and flex-start put all
     9.6px of that slack on the right — so the set sat 10px left of centre.
     Centring splits it.

     Declared twice on purpose. `safe center` means "centre, but fall back to
     start rather than overflow", which is what protects the narrow widths
     where the row scrolls: centred overflow pushes the first item off the
     left edge where it cannot be scrolled back to. It needs Safari 15.4+, so
     the plain `center` above it is what Safari 15.0-15.3 keeps — same
     centring, without the overflow guard. Desktop is unaffected either way:
     the auto margins there consume the free space first, which makes
     justify-content a no-op. D77. */
  justify-content: center;
  justify-content: safe center;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;

  /* The scrollbar itself is noise on a 45px strip; the clipped last item is
     the affordance that says "there is more this way". */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.nav__links::-webkit-scrollbar { display: none; }

/* --------------------------------------------------------------------------
   Services disclosure, D61.

   The nav had 4px of slack at 375px, so a sixth top-level item pushed 102px
   off-screen — all of "Contact" and part of "About". Nesting the deliverables
   link under Services keeps the row at 331px and unchanged.

   The trigger is a <button>, not a link. A control that opens something is a
   button; screen readers announce the expanded state from aria-expanded, and
   keyboard users get Space and Enter for free. "All services" is the first
   item so the section itself is still reachable — that is what the old
   top-level link did, and removing it without replacement would have been a
   regression dressed as a feature.
   -------------------------------------------------------------------------- */
.nav__has-menu { position: relative; }

.nav__disclosure {
  display: inline-flex;
  align-items: center;
  /* space-1, not space-2: the chevron's own box already carries visual gap,
     and at space-2 the row measured 345px against 335px of space. */
  gap: var(--space-1);
  min-height: var(--tap-target);
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: var(--text-small);
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
}

/* Drawn in CSS rather than shipped as an asset — one less request, and it
   inherits currentColor so it follows the theme with no second definition. */
.nav__chevron {
  width: 0.4em;
  height: 0.4em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-0.1em, -0.1em);
  transition: transform var(--transition);
}

.nav__disclosure[aria-expanded="true"] .nav__chevron {
  transform: rotate(-135deg) translate(-0.1em, -0.1em);
}

.nav__menu {
  list-style: none;
  margin: 0;
  padding: var(--space-2) 0;
  background: var(--ground);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--nav-shadow);
}

.nav__menu[hidden] { display: none; }

.nav__menu a {
  display: flex;
  align-items: center;
  min-height: var(--tap-target);
  padding: 0 var(--space-4);
  font-size: var(--text-small);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}

.nav__menu a:hover,
.nav__menu a:focus-visible { background: var(--surface); color: var(--accent); }

/* Mobile: opens as a panel below the bar rather than a floating layer. The
   nav row scrolls sideways, and a menu anchored inside a scrolling container
   travels with it — off-screen the moment the user swipes. */
.nav__menu {
  position: fixed;
  left: var(--gutter);
  right: var(--gutter);
  top: var(--nav-height);
  z-index: 55;
  max-height: calc(100vh - var(--nav-height) - var(--space-5));
  overflow-y: auto;
}

@media (min-width: 48em) {
  .nav__menu {
    position: absolute;
    top: calc(100% + var(--space-2));
    left: 0;
    right: auto;
    min-width: 15rem;
    max-height: none;
  }
}

.nav__links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
  color: var(--ink);
  font-size: var(--text-small);
  text-decoration: none;
  white-space: nowrap;        /* never break a label mid-scroll */
}
.nav__links a:hover { color: var(--accent); }

/* Which section you are actually in. Colour alone was carrying this and it was
   not enough to notice — which is also WCAG 1.4.1: a state signalled only by
   hue is invisible to anyone who cannot separate that hue from --ink, and easy
   to miss for everyone else. The underline adds a change of SHAPE, so the
   indicator survives being read in greyscale.

   text-decoration rather than a border or a pseudo-element: these links are
   44px-tall flex boxes (the tap target), so a border-bottom would draw a rule
   at the bottom of the box rather than under the words. text-decoration tracks
   the text itself and shifts no layout. text-underline-offset and
   -thickness are both Safari 12.1+, inside the US-060 floor.

   Distinct from :hover on purpose — hover changes colour, current changes
   colour AND gains the rule, so the two never read as the same state. D76. */
.nav__links > li > a[aria-current] {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-decoration-thickness: 2px;
  text-underline-offset: 6px;
}

/* Merriweather, restored. D73 tried Raleway bold on the grounds that the sans
   is more legible at 14px; that was true and still lost. The serif is the only
   one in the bar, and being the only one is what makes this read as a mark
   rather than as a sixth menu item — authority the weight alone could not
   carry. The link behaviour D73 added is unaffected and stays. D75. */
.nav__name {
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  font-size: var(--text-small);
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
  align-items: center;
  min-height: var(--tap-target);
}

.nav__name:hover { color: var(--accent); }

@media (min-width: 48em) {    /* 768px */
  .nav__name,
  .nav .cta { display: inline-flex; }
  .nav__links {
    gap: var(--space-5);
    /* Was 0 0 auto, which let space-between park the whole row hard right
       against the vacated button slot. Growing instead lets the row own the
       width so the last item can be pushed to the far edge. D66. */
    flex: 1 1 auto;
    overflow-x: visible;
  }

  /* Removing the header button left a hole on the right of the desktop bar.
     Contact fills it: the section anchors stay grouped beside the name and
     the conversation-starter sits alone at the edge, which is roughly where
     the eye had learned to look for it.

     Desktop only, and CSS only. On mobile the bar is a single scrolling
     anchor row (D46) and Contact stays in sequence — pushing it to an edge
     there would strand it past the fold of a horizontal scroll. One element,
     two positions, no duplicated link. D66. */
  /* Two auto margins, not one. With only .nav__end pushed, the free space all
     collected on its left and the four section anchors slid up against the
     name — the bar read as left-weighted where it used to read as balanced.
     A second auto before the first anchor splits that space in half, which
     restores the group to the middle it occupied when the header button was
     still holding the right end. D67. */
  .nav__links > li:first-child { margin-left: auto; }
  .nav__end { margin-left: auto; }
}


/* ==========================================================================
   8. FOOTER — US-010
   The .footer--no-home variant renders the owner's name as plain text with
   no link, which is the mechanism the two-audience separation rests on.
   ========================================================================== */

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

.footer__name {
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  font-size: var(--text-h3);
  color: var(--ink);
}

.footer__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

.footer__links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-target);
}


/* ==========================================================================
   9. EXPANDER — US-011
   Native <details>/<summary>. Opens with JavaScript disabled, is keyboard
   operable for free, and its full text is present in the served HTML before
   any script runs — which is what FR-45 and the crawlers require.
   ========================================================================== */

.expander {
  border-top: 1px solid var(--rule);
}
.expander:last-of-type { border-bottom: 1px solid var(--rule); }

.expander__summary {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  /* US-011: the whole summary row is the hit area. */
  width: 100%;
  padding-block: var(--space-5);
  min-height: var(--tap-target);
  cursor: pointer;
  list-style: none;
}

/* Remove the default disclosure triangle in both engines. */
.expander__summary::-webkit-details-marker { display: none; }
.expander__summary::marker { content: ""; }

.expander__heading {
  flex: 1 1 auto;
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  font-size: var(--text-h3);
  line-height: var(--leading-heading);
  color: var(--ink);
  margin: 0;
}

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

/* Once open, the teaser is redundant with the full text below it. */
.expander[open] .expander__teaser { display: none; }

/* Chevron drawn in CSS, not an icon set — US-006 prohibits loading an icon
   font or SVG icon set. */
.expander__chevron {
  flex: 0 0 auto;
  width: 0.6em;
  height: 0.6em;
  margin-top: 0.45em;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: rotate(45deg);
  transform-origin: center;
  transition: transform var(--transition);
}

.expander[open] .expander__chevron {
  transform: rotate(-135deg);
}

.expander__body {
  padding-bottom: var(--space-6);
}
.expander__body > * + * { margin-top: var(--space-4); }


/* ==========================================================================
   10. MOTION — US-012
   Default: chevron rotates, colours transition.
   Reduced: everything resolves instantly. The rotation still happens, it
   just is not animated — the open state must stay legible.
   ========================================================================== */

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


/* ==========================================================================
   11. HERO — US-015, US-016
   At 375x667 the headline, subhead and CTA must all sit above the fold, with
   the photograph below it. The four-way conflict between FR-2, FR-3, FR-20
   and FR-38 was resolved at GAP-060 by pushing the photograph down; this
   block is where that resolution is enforced.
   ========================================================================== */

/* The hero is a tinted panel at page width, not a full-bleed band. Because
   .wrap is `width:100%; max-width:--shell`, the same declaration gives both
   behaviours for free: below 872px the panel is the viewport, above it the
   panel is the container with ground either side. No media query, and no thin
   white margins on a phone, which is what a fixed-width tint would have left.

   Deliberately the opposite treatment to .section--band on contact: that one
   breaks the container to close the page, this one respects it to open it.
   Contained reads as a masthead, full-bleed reads as a footer.

   The foot padding drops from --space-8 to --space-6. The whitespace used to
   be the only thing separating hero from Problem; the tint does that job now,
   so the gap can stop doing it twice. D69. */
.hero {
  padding-block: var(--space-7) var(--space-6);
  background: var(--surface);
  border-radius: var(--radius);
}

.hero__headline {
  margin-bottom: var(--space-5);
}

.hero__subhead {
  color: var(--muted);
  margin-bottom: var(--space-6);
}

.hero__cta {
  margin-bottom: var(--space-4);
}

/* The photograph is deliberately absent until a real one exists — EH-6 makes
   a shipped placeholder a launch failure, and the decision was to ship none
   rather than a stand-in. No reserved box, so the layout holds with no empty
   gap and no broken-image icon (US-016). */
.hero__portrait {
  margin-top: var(--space-8);
  max-width: 320px;
  border-radius: var(--radius);
}


/* ==========================================================================
   12. SERVICE TIERS — US-020, US-021
   Visual weight increases across the three: plain, bordered, filled. No tier
   carries a "recommended" badge — the ladder is commitment, not a
   sales ranking.
   ========================================================================== */

.tiers {
  display: grid;
  gap: var(--space-5);
}

.tier {
  padding: var(--space-5);
  border-radius: var(--radius);
}

.tier__name {
  margin-bottom: var(--space-3);
}

.tier__meta {
  color: var(--muted);
  font-size: var(--text-small);
  margin-block: var(--space-4);
}

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

/* Tier 1 — plain. No border, no fill; it is the lightest commitment. */
.tier--plain {
  padding-inline: 0;
}

/* Tier 2 — bordered. */
.tier--bordered {
  border: 1px solid var(--rule);
}

/* Tier 3 — filled, with a heavier heading. */
.tier--filled {
  background: var(--surface);
}

/* US-020 requires this tier to carry "a heavier heading". It was set to
   --text-h2, which is a jump from 20px to 28px — 40% at identical card
   widths, which read as a mistake rather than as emphasis. --text-h2-tight is
   a 10% step: clearly heavier beside the other two, and no longer competing
   with the section's own h2 two inches above it.

   The accent colour carries the rest of the emphasis, so the escalation no
   longer rests on size alone. D62. */
.tier--filled .tier__name {
  font-size: var(--text-h2-tight);
  color: var(--accent);
}


/* ==========================================================================
   13. HOW IT WORKS — US-026
   Four sequential steps. Numbered by CSS counter rather than a list marker,
   so the number is decorative and screen readers get the heading text.
   ========================================================================== */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-6);
}

.step {
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr);
  gap: var(--space-4);
  align-items: start;
}

.step__name {
  margin-bottom: var(--space-2);
}

/* Bespoke line icons drawn in the markup — not an icon font and not a
   third-party set, so nothing extra is loaded and US-006's "no icon set"
   clause is not triggered by a network request. They inherit --accent
   through currentColor, so both themes are handled for free. */
.step__icon {
  width: 40px;
  height: 40px;
  color: var(--accent);
  flex: 0 0 auto;
}


/* ==========================================================================
   14. CONTACT — US-027
   No form element anywhere: FR-25 excludes capture mechanisms, and the
   mailto is the only non-Calendly route (FR-19).
   ========================================================================== */

/* A tint at page width, matching the hero panel exactly. It was full-bleed at
   D68 — box-shadow spread plus clip-path to break the container — on the
   argument that contained reads as a masthead and full-bleed reads as a
   footer. The owner preferred the two panels to agree, so that distinction is
   withdrawn and the page now opens and closes on the same shape. Because
   .section is also a .wrap, the plain background gives page width on desktop
   and full width on a phone with no media query. D68, amended D74. */
.section--band {
  background: var(--surface);
  border-radius: var(--radius);
}

/* The tint is the separation; a rule on top of it would be belt and braces. */
.section + .section--band { border-top: 0; }

.contact__routes {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-5);
  margin-top: var(--space-6);
}


/* ==========================================================================
   15. MEDIA — US-016 and the image slots added 2026-08-23 (D43)

   Every image carries width/height in the markup so the box is reserved
   before the file arrives. Without that, each image shifts the page as it
   loads and Cumulative Layout Shift drags the US-061 Performance score down.
   ========================================================================== */

.media {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius);
  background: var(--surface);
}

/* The layout classes moved onto <picture> for the two portraits (US-016
   wants a JPEG fallback). <picture> is inline by default and would not take
   a grid placement or a width, so it needs an explicit block box. */
.hero__media,
.about__media { display: block; }

.media--square { aspect-ratio: 1 / 1; object-fit: cover; }
.media--portrait { aspect-ratio: 4 / 5; object-fit: cover; }
/* 3/1 as of D56 — the service photographs are supplied in that ratio. The
   files are 1616x534, a hair over 3:1, so `cover` trims about 0.9% off the
   sides. Imperceptible, and it keeps all three boxes identical. */
.media--wide { aspect-ratio: 3 / 1; object-fit: cover; }


/* ==========================================================================
   16. HERO LAYOUT — US-015, US-016, as amended by D43 and D46

   One <img>, two placements, driven by grid rather than duplicated markup:

   Mobile   headline and subhead across the top, then a single row of
            CTA on the left and an 80px portrait on the right. The portrait
            is a presence marker at that size, not the credibility shot —
            that one lives in About, where there is room for it.

   Desktop  text column and full portrait side by side, both above the fold.
   ========================================================================== */

/* Mobile row 2 is [portrait][button over name]: the portrait on the left, and
   on the right edge a stacked block of the button with the name beneath it.
   For one-handed right-thumb use — about half of mobile sessions — the
   bottom-right is the easiest reach, so the button keeps the good corner.
   The name moved under the button because the button is 44px against the
   portrait's 80px: top-aligning the two leaves 36px of dead space below the
   button, and the name's 19px line fits inside it. The row height stays 80px,
   driven by the portrait, so the name costs no height at all — which matters,
   because a dedicated CTA row would push the button past the visible edge on
   a small iPhone once Safari's chrome is accounted for, and FR-2 makes that
   unacceptable. D50. */
.hero__inner {
  display: grid;
  grid-template-columns: 80px minmax(0, 1fr);
  column-gap: var(--space-3);
  row-gap: var(--space-6);
  align-items: start;
}

/* Rows are stated explicitly. Relying on auto-placement here put the
   portrait on a row of its own instead of beside the button. */
.hero__copy { grid-column: 1 / -1; grid-row: 1; }

.hero__media {
  grid-column: 1;
  grid-row: 2;
  align-self: end;   /* see .hero__ident — shared baseline, D66 */
  width: 80px;
  height: 80px;
}

/* Button and name as one grid child rather than two. Grid has no per-row gap
   control, so keeping them in separate rows would have forced the 2rem row
   gap between them — or spanning the portrait across both rows, which makes
   the rows sum to the portrait's 80px and pushes the name away from the
   button. One flex column sidesteps both. */
.hero__ident {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--space-2);

  /* Portrait, button and name share a floor. The column stretches to the row
     height and pushes its two children to the ends, so the name's last line
     lands on the portrait's bottom edge instead of floating in the middle of
     a top-aligned stack.

     Paired with align-self:end on the portrait, this holds whichever of the
     two is taller. Normally the 80px portrait sets the height and the name
     drops to meet it; if the name ever wraps and the column wins, the
     portrait rises to meet the name instead. Either way the bottom edges
     agree, which is the thing being asked for. D66. */
  align-self: stretch;
  justify-content: space-between;
}

/* Mobile-only branding. The desktop nav already carries the name, and the
   desktop hero has a full portrait — a name label there would be a third
   repetition. No tagline under it either: the h1 two inches above already
   says what the work is. */
.hero__name {
  /* No longer width-gated. Beside the button the name had 78px and wrapped to
     two lines, and below 360px it had 23px and was hidden outright. Under the
     button it reads "with Blessit George" and needs 136px, against 188px
     available even at 320px — one line on every phone, so smallest-screen
     users get the branding back.

     The lead-in does the work that styling alone could not: small text under a
     button is the web's convention for a disclaimer, and no disclaimer opens
     with "with". It completes the button's sentence instead. Full --ink weight
     for the same reason — muted grey here would read as fine print.

     Deliberately OUTSIDE the <a>: the link's accessible name stays exactly
     "Book a call", which US-008 requires of every instance. D50. */
  text-align: right;
  margin: 0;
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  font-size: var(--text-small);
  line-height: 1.25;
  color: var(--ink);
}

/* A connector, not the payload. Set at the same size and colour but in the
   body face at body weight, so the eye lands on the name and "with" reads as
   grammar. Bold here would claim equal importance with the name. */
.hero__name-lead {
  font-family: var(--font-body);
  font-weight: var(--weight-body);
}

.hero__cta {
  margin-bottom: 0;
}

@media (min-width: 48em) {   /* 768px */
  /* Reverted to the pre-D69 arrangement at the owner's request: headline and
     subhead together in the left column, portrait beside them in its own,
     vertically centred against the pair. D69's masthead — headline across the
     full container, subhead and button bottom-aligned against the portrait —
     measured well and read badly, which is the whole reason a layout gets
     looked at rather than calculated. D70.

     What D69 leaves behind: the --surface panel and the tightened foot
     padding. Those were separate requests and are untouched. */
  .hero__inner {
    grid-template-columns: 1fr 1fr;
    column-gap: var(--space-7);
    /* Was --space-6 (32px), which measured as 56px on the page — and shrinking
       the gap alone barely moved it, because most of that distance was not
       the gap. The 380px portrait spans both rows, so it forces them taller
       than their content and the slack lands wherever the rows put it: right
       between the last line of description and the button.

       Four rows fix it without giving up the centring. The copy and the button
       take rows 2 and 3 at their own heights, so the space between them is
       exactly the row gap and nothing else. Rows 1 and 4 are each `1fr`, so
       the height the portrait forces is split equally above and below the
       pair — which leaves the text block centred against the portrait rather
       than pinned to one end of it.

       Dumping the slack on a single side would have been simpler and would
       have put the text about 15px off the portrait's centre line. Both of
       the owner's asks are satisfiable at once; only one of them was
       satisfiable by shrinking the gap. D70. */
    grid-template-rows: 1fr auto auto 1fr;
    row-gap: var(--space-4);
  }
  .hero__copy  { grid-column: 1; grid-row: 2; }
  .hero__subhead { margin-bottom: 0; }
  .hero__ident { grid-column: 1; grid-row: 3; justify-self: start; align-self: start; }
  .hero__name  { display: none; }
  .hero__media {
    grid-column: 2;
    grid-row: 1 / 5;
    width: 100%;
    height: auto;
    align-self: center;
  }
}


/* ==========================================================================
   15c. WORK SAMPLES — D59

   Two anonymised backlogs, linked from both audiences. The cards are built
   from the same tokens as everything else, so each page renders them in its
   own theme with no per-page rules: light on /, dark on /agencies.
   ========================================================================== */

.samples {
  display: grid;
  gap: var(--space-5);
}

@media (min-width: 48em) {
  .samples { grid-template-columns: 1fr 1fr; gap: var(--space-6); }
}

/* The whole card is the link. A card with a small "read more" inside it gives
   a 44px target inside a 300px one people are already trying to click. */
.sample {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--ground);
  color: inherit;
  text-decoration: none;
  transition: border-color var(--transition), box-shadow var(--transition);
}

.sample:hover,
.sample:focus-visible {
  border-color: var(--accent);
  box-shadow: var(--nav-shadow);
}

.sample__kind {
  margin: 0;
  font-size: var(--text-small);
  font-weight: var(--weight-strong);
  color: var(--accent);
}

.sample__name {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: var(--weight-heading);
  font-size: var(--text-h3);
  color: var(--ink);
}

.sample__desc {
  margin: 0;
  font-size: var(--text-small);
  color: var(--muted);
}

/* Figures pulled from the documents themselves, not rounded for effect. */
.sample__stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  padding: 0;
  list-style: none;
  margin-top: auto;
  padding-top: var(--space-3);
  border-top: 1px solid var(--rule);
}

.sample__stats li {
  font-size: var(--text-small);
  color: var(--muted);
}

.sample__stats b {
  font-weight: var(--weight-strong);
  color: var(--ink);
}

/* Carries the affordance the removed underline used to. */
.sample__open {
  margin: 0;
  font-size: var(--text-small);
  font-weight: var(--weight-strong);
  color: var(--accent);
}


/* ==========================================================================
   16b. FLOATING CTA — mobile only, D46

   FR-21 requires a persistent booking button. This one hides itself, but
   only ever while another "Book a call" is already on screen — so a booking
   route is visible at every scroll position, which is what FR-21 protects.

   Default state is VISIBLE. The script only ever adds the hiding, so with
   JavaScript off the button simply stays put and nothing breaks.
   ========================================================================== */

/* Scroll-to-top — mobile only. D74.

   Outlined rather than accent-filled. Both treatments clear WCAG AA and by a
   similar margin (accent on --surface is 4.67:1, --on-accent on --accent is
   4.99:1), so contrast did not decide it — hierarchy did. Two accent-filled
   buttons on screen at once read as equally important, and this one is
   utility navigation sitting beside the page's only conversion action. The
   outline puts it a tier below without making it hard to see.

   The fill matters even though it nearly matches the page: it stops body text
   showing through the button, and the shadow lifts it off both --ground and
   the --surface panels it passes over. */
.to-top {
  position: fixed;
  left: var(--gutter);
  bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-target);
  height: var(--tap-target);
  border: 2px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--accent);
  font-size: 1.25rem;
  line-height: 1;
  text-decoration: none;
  box-shadow: 0 2px 12px rgb(0 0 0 / 0.18);
  transition: opacity var(--transition), transform var(--transition);
}

/* Same contract as .floating-cta: visible by default, the script only ever
   adds the hiding. With JavaScript off the button stays put and still works —
   it is a plain fragment link. Pointless at scroll-top, never broken. */
.to-top[data-hidden="true"] {
  opacity: 0;
  transform: translateY(8px);
  visibility: hidden;
  pointer-events: none;
}

/* Desktop has the nav name for this (D73) and does not need a second control. */
@media (min-width: 48em) {
  .to-top { display: none; }
}

.floating-cta {
  position: fixed;
  right: var(--gutter);
  bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  z-index: 60;
  box-shadow: 0 2px 12px rgb(0 0 0 / 0.18);
  transition: opacity var(--transition), transform var(--transition);
}

/* visibility:hidden (not display:none) so it leaves the tab order while
   hidden but can still animate back in. */
.floating-cta[data-hidden="true"] {
  opacity: 0;
  transform: translateY(8px);
  visibility: hidden;
  pointer-events: none;
}

/* Desktop keeps it too, since D66 removed the header button that used to be
   the desktop's persistent route. Nudged clear of the mobile position: there
   is room for a wider margin, and the lower-right corner is where the
   Deliverables cards and the sample tiles end. */
@media (min-width: 48em) {
  .floating-cta {
    right: var(--space-6);
    bottom: var(--space-6);
  }
}


/* ==========================================================================
   17. SECTION MEDIA — services, about, steps (D43)
   ========================================================================== */

.tier__media {
  margin-bottom: var(--space-4);
}

.about__inner {
  display: grid;
  gap: var(--space-6);
}

/* Mobile: the portrait sits directly under the "About me" heading, ahead of
   the copy. Done with `order` rather than by moving the element, so the DOM
   keeps the sequence the desktop grid needs — text into column 1, portrait
   into column 2 — and only the mobile painting order changes. D63.

   Reordering visually away from DOM order is safe here specifically because
   nothing in this section is focusable: a <picture> takes no tab stop, so
   there is no way for the keyboard order and the visual order to disagree.
   The same trick around a link or a button would be a real accessibility
   bug. */
.about__media {
  order: -1;
  /* Full width, not the 280px cap it carried while it sat below the copy as a
     small aside. Promoted to a block under the heading, a 280px image in a
     335px column left 55px empty down its right side — the same
     left-aligned-with-a-gap look D49 rejected in the body copy. Costs 69px of
     height (350 -> 419) before the reader reaches the first word. Owner's
     call, D64. */
}

@media (min-width: 48em) {
  .about__inner {
    grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
    gap: var(--space-7);
    align-items: start;
  }
  .about__media {
    order: 0;
    max-width: none;
    position: sticky;
    top: calc(var(--nav-height) + var(--space-5));
  }
}


