/* ==========================================================================
   site.css — the 2026 redesign layer: home splash + the new site navbar.
   Loads AFTER gp-ecom.webflow.css (cascade rank "pages"), so it overrides the
   Webflow export without editing it. See docs/CASCADE.md.
   ========================================================================== */

/* These are ALIASES, not values. Every one resolves to a token in tokens.css,
   which is generated from design/tokens.json. Change a colour there and it
   changes here, in the Webflow layer that also consumes it, and in the design
   guide, from one edit. Do not put a literal hex in this file.

   Two values moved when this layer was pointed at the tokens:
     --gp-cream  #fff7ef -> #FFF8EE   collapses a near-duplicate; body already
                                       used #FFF8EE, so this removes a seam
     --gp-ink    #1a1a1a -> #303030   adopts the site's standard body ink,
                                       which 21 other declarations already use */
/* ---------------------------------------------------------------- Space Mono
   MNM is replaced by Space Mono, which is what gp_app/design/tokens.json
   already declares for its mono family — so the site and the app now agree.

   SIL Open Font License 1.1, verified from the font's own name table
   ("This Font Software is licensed under the SIL Open Font License, Version
   1.1"), so bundling and redistributing it is permitted. Files copied from
   gp_app/fonts rather than fetched, which keeps the site hermetic: no CDN
   reference, no runtime request off-origin. See CLAUDE.md.

   TWO declarations on purpose. The first names Space Mono for this layer. The
   second REDEFINES MNM to the same files, so the 14 MNM declarations inside
   gp-ecom.webflow.css follow without editing the monolith — site.css loads
   after it, and the later @font-face for a given family and weight wins.

   Only 400 and 700 upright are declared, because that is all that is used:
   measured across five pages, MNM renders 798 characters at normal 400 and
   435 at normal 700, and zero italic. The two MNM italic files are now dead
   weight and can be dropped when the font set is next pruned. */
@font-face{font-family:'Space Mono';src:url('../fonts/SpaceMono-Regular.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap;}
@font-face{font-family:'Space Mono';src:url('../fonts/SpaceMono-Bold.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap;}
@font-face{font-family:MNM;src:url('../fonts/SpaceMono-Regular.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap;}
@font-face{font-family:MNM;src:url('../fonts/SpaceMono-Bold.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap;}

:root{
  --gp-cream:     var(--gp-background-default);
  --gp-maroon:    var(--gp-accent-maroon);
  --gp-brown:     var(--gp-text-muted);
  --gp-rule:      var(--gp-border-hairline);
  --gp-underline: var(--gp-accent-red);
  --gp-ink:       var(--gp-text-primary);
}

/* ---------------------------------------------------------------- home splash
   A cover screen: cream ground, logo fading in, nav beneath. No navbar, no
   footer — this page is the front door and carries nothing else. */
.splash{
  min-height:100vh;display:flex;flex-direction:column;
  align-items:center;justify-content:center;
  background:var(--gp-cream);padding:24px;box-sizing:border-box;
}
/* filter:url() references the inline <svg><filter id="gpTint"> in index.html —
   it must live in the same document, so it cannot move into this stylesheet. */
.splash__logo{
  width:min(600px,80vw);height:auto;display:block;
  /* No url(#gpTint) any more — the filter and the two-tone lockup it recoloured
     both went on 8 Sep. A dangling filter reference is not harmless: it can
     stop the element rendering altogether, so it had to leave here in the same
     change. The shadow stays; it is what separates the lockup's black outline
     from a dark photograph. */
  filter:drop-shadow(0 2px 22px rgba(0,0,0,.55));
  /* Fade only, no rise. gpFadeUp is a to-only keyframe, so the movement came
     from this element's own transform rather than from the animation — the nav
     below declares no transform and so never moved. Dropping it here leaves
     the keyframe animating translateY(0) from none, which is no motion. */
  opacity:0;
  animation:gpFadeUp 2400ms cubic-bezier(.22,.61,.36,1) 150ms forwards;
}
.splash__filter{position:absolute;width:0;height:0;overflow:hidden;}

/* ── Splash background: rotating store photography ──────────────────────────
   Two stacked layers crossfading every 5s, added 8 Sep 2026.

   THE TREATMENT IS ON THE PARENT, the blur on the children. That split is what
   keeps the keyframes readable: they animate only opacity, blur and scale, and
   the colour grade is stated once here rather than repeated in every stop.

   The grade is not decoration, it is what makes the wordmark legible. Measured
   across the six photographs, the centre band where the lockup sits runs at
   mean luminance 100 to 149, 90th percentile 155 to 204. brightness(.625),
   raised 25% from .5 on 8 Sep, puts that band between roughly 63 and 93.

   🔴 TWO to watch, and they are hard in different ways. splash-03 has the
   highest MEAN, 149, so it is bright everywhere and the lockup has the least
   separation overall. splash-06 has a comfortable mean of 110 but the highest
   HIGHLIGHTS of the set at p90 204 — sky and a pale yellow awning — so it is
   bright in patches rather than throughout, and the danger is a hot area
   landing behind part of the wordmark rather than the whole thing washing out.
   Mean is not enough on its own; check both figures.
   RE-MEASURE WHEN NEW PHOTOGRAPHS GO IN — one grade does not fit every image,
   and it is the worst case that decides, not the average. */
.splash{position:relative;overflow:hidden;}
.splash__bg{
  position:absolute;inset:0;z-index:0;overflow:hidden;pointer-events:none;
  /* No filter. The stack here ran grayscale/sepia/saturate/brightness/contrast
     to push the interiors towards the retro treatment; it came off in stages
     (brightness up 25%, then 50%, then the whole stack softened 15%) and is now
     removed, so the photographs render as shot. The logo's drop-shadow, the nav's
     text-shadow and .splash__scrim are what hold contrast now — they were doing
     part of that job already, and all of it once the darkening went. */
}
.splash__bg-img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  opacity:0;will-change:opacity;
}
/* Crossfade only. The drift and the defocus that were here on the first pass
   are gone at James's request, 8 Sep — opacity is the only thing that moves.

   Worth knowing what went with them: the outgoing defocus was doing contrast
   work as well as looking nice. Mid-swap is when two photographs overlap and
   the tonal range behind the lockup is least predictable, and a blur flattens
   local contrast exactly then. With a plain crossfade the grade and the scrim
   carry that alone, so a bright photograph is likelier to show through the
   swap than it was. Re-check the middle of a transition when new images land,
   not just the held frames. */
.splash__bg-img.is-on{animation:gpSplashIn 1400ms ease forwards;}
.splash__bg-img.is-off{animation:gpSplashOut 1400ms ease forwards;}
@keyframes gpSplashIn  { from { opacity:0; } to { opacity:1; } }
@keyframes gpSplashOut { from { opacity:1; } to { opacity:0; } }
/* Belt and braces over the grade — a soft pool of shade under the lockup that
   clears toward the edges, so one unlucky photograph cannot break the page. */
/* The scrim arrives WITH the photograph, on the same 1400ms curve.
   It is a radial-gradient, so painted at full strength on a cream page with no
   image under it yet it is a dark vignette in the middle of the screen — and
   because the photograph then fades up beneath it over 1.4s, the whole arrival
   read as a radial wipe from the centre out. Nobody designed that; it was the
   scrim being instant while the thing it exists to darken was not.
   `both`: the backwards fill holds it transparent through any delay, so there
   is no frame of vignette-on-cream. */
.splash__scrim{
  animation:gpSplashIn 1400ms ease both;
  position:absolute;inset:0;z-index:1;pointer-events:none;
  background:radial-gradient(58% 52% at 50% 44%,
    rgba(0,0,0,.42) 0%, rgba(0,0,0,.22) 55%, rgba(0,0,0,0) 100%);
}
/* The lockup and links sit above both. */
.splash__logo,.splash__nav{position:relative;z-index:2;}
@media(prefers-reduced-motion:reduce){
  /* No rotation and no drift, but the first photograph still shows — the page
     would look broken without it, and splash-bg.js skips its timer to match. */
  .splash__bg-img{animation:none;}
  .splash__bg-img.is-on{opacity:1;}
}
/* left:10px is an optical centring nudge — the leading divider makes the row
   read left-of-centre. Uses `left`, not transform: the fade owns transform. */
.splash__nav{
  display:flex;flex-wrap:wrap;justify-content:center;
  margin-top:82.5px;position:relative;left:10px;
  opacity:0;animation:gpFadeUp 1100ms cubic-bezier(.22,.61,.36,1) 1700ms forwards;
}
/* The front door introduces itself once per visit. The staged reveal — the logo
   over 2400ms, the nav held back to 1700ms — is a first-impression thing and
   does not replay.

   Later arrivals are not static, though: with the intro simply cancelled the
   logo was painted at full strength on the very first frame, which lands harder
   than no animation at all. They get one short fade instead, both elements
   together with no stagger, so it reads as the page arriving rather than as the
   intro running again. gp-seen is set by the head script before body renders,
   so whichever of the two applies is the first thing painted.

   `both` is what makes it work: the intro's start state (opacity:0) lives on
   the elements themselves, so the backwards fill holds them hidden until the
   fade begins, and the forwards fill leaves them visible after. Cancelling the
   animation without restoring opacity would strand them invisible. */
html.gp-seen .splash__logo,
html.gp-seen .splash__nav{
  animation:gpFadeUp 650ms var(--gp-motion-easing) both;
}
/* No fade either way, but they must still be visible — their own opacity:0
   would otherwise strand them with the animation switched off. */
@media(prefers-reduced-motion:reduce){
  .splash__logo,.splash__nav,
  html.gp-seen .splash__logo,html.gp-seen .splash__nav{animation:none;opacity:1;}
  /* No fade, but it must still be painted — its own animation carries the
     opacity, so switching that off without restoring it strands the scrim
     invisible and the logo loses its contrast. */
  .splash__scrim{animation:none;opacity:1;}
}
.splash__nav a{
  font-family:var(--gp-font-mono), sans-serif;text-transform:uppercase;
  color:var(--gp-maroon);text-decoration:none;letter-spacing:var(--gp-tracking-normal);
  font-size:.85rem;padding:6px 20px;position:relative;white-space:nowrap;
}
/* The links were maroon, which was right on cream and invisible on a darkened
   photograph. Stated HERE rather than with the rest of the background rules
   above, because those sit earlier in the file than the original
   `.splash__nav a` and an equal-specificity rule that comes first simply
   loses — which is exactly what happened on the first attempt. */
.splash__nav a{color:var(--gp-accent-cream);}
.splash__nav a:hover{color:var(--gp-accent-cream);opacity:.7;}
/* The logo carries a drop-shadow so it survives a light photo behind it; the
   nav had nothing, which only worked while the backgrounds were dark. Raising
   brightness took the cream against the brightest of the six images
   (splash-03) from roughly 4.8:1 to 2.5:1 — through and then well past the 4.5:1
   AA floor for text this size. A shadow buys the contrast back without
   darkening the photography again, which is the point of the brightness. */
.splash__nav{text-shadow:0 1px 10px rgba(0,0,0,.55), 0 0 2px rgba(0,0,0,.45);}
/* The dividers take the links' own colour rather than the hairline grey, which
   was pitched for cream and disappears on a photograph. currentColor rather
   than the token repeated, so they cannot drift apart from the links again. */
.splash__nav a + a::before{
  content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);
  height:1.05em;width:1px;background:currentColor;opacity:.45;
}
.splash__nav a:hover{opacity:.6;}

@keyframes gpFadeUp{to{opacity:1;transform:translateY(0);}}

/* --------------------------------------------------------------- site navbar
   Fixed, transparent, hairline rule beneath. Markup: partials/navbar-site.html

   The Webflow navbar is hidden rather than deleted — its elements carry the
   data-w-id and id="w-node-*" attributes that IX2 binds to and that
   scripts/verify.sh counts. See the partial for the full reasoning. */
.navbar_component{display:none !important;}

/* 🗑️ The rule that used to sit here is gone, and so is what it hid.
   The Webflow footer was DELETED from all five pages on 10 Sep 2026 —
   backlog 38. This rule hid it instead, and the reason it gave was that cutting
   the markup would drop the grid-node count by 40. That was true, and it was
   never a reason not to do it: the count is a gate, and a gate is updated in the
   same commit as the change it measures. It was, 155 → 115.

   Hiding it cost ~4.6 KB per page, on five pages, downloaded by everyone and
   seen by nobody — and it left ten empty-fragment anchors in the markup that
   read as live dead links to any grep. Two audit findings were raised and
   retracted on 10 Sep because of exactly that.

   The visible footer is partials/site-footer.html (`.gp-ft`) on six pages.
   The splash has none by design and must keep none. */

.gp-hd{
  position:fixed;top:0;left:0;right:0;z-index:1000;
  transition:opacity .35s ease, transform .45s cubic-bezier(.4,0,.2,1);
}
/* scrolled off the top: slide up and fade out. Returns only back at the top. */
.gp-hd--away{opacity:0;transform:translateY(-100%);pointer-events:none;}
/* The navbar introduces itself only when the page before it had none — in
   practice, arriving from the splash, where it would otherwise appear out of
   nothing. Between two pages that both carry a navbar it must NOT animate: the
   drop's 420ms hidden delay reads as the header disappearing and coming back,
   when what should happen is that it simply stays put. data-nav-in is set by
   the head script from a flag the previous page left behind; see
   gp-view-transitions.js. The delay is the enter slide's own 420ms, so the drop
   starts as the page lands.

   `backwards` and NOT `forwards`, which is the whole trick. .gp-hd--away moves
   the navbar with opacity and transform, and a forwards fill would keep the
   animation's end values winning over that declaration for the life of the
   page — the header would never hide on scroll again. Backwards fills only
   during the delay, then releases, and since the end state is identical to the
   declared one there is nothing to jump. */
@keyframes gpNavDown{
  from{ opacity:0; transform:translateY(-100%); }
  to  { opacity:1; transform:none; }
}
html[data-nav-in] .gp-hd{
  animation:gpNavDown 420ms cubic-bezier(.22,.61,.36,1) 420ms backwards;
}

/* ── About: navy navbar, and no announcement bar ────────────────────────────
   About's first screen is a bright photograph, and the navbar's warm grey sits
   on top of it with almost no contrast. On that page only, the ink fades to
   --gp-accent-navy once the header has settled, and fades back on the way out.
   The container stays transparent throughout — only the ink moves.

   The logo and the Instagram glyph are <img>, so `color` cannot reach them.
   Both SVGs are a single fill of #ACA397, which is exactly --gp-rule, so they
   are masked instead: the anchor becomes the silhouette and its
   background-color paints it. That gives the real token colour rather than a
   filter chain approximating navy, and background-color transitions, so they
   fade in step with the text. mask-origin is content-box because .gp-hd__logo
   carries a padding-top and the image is centred inside the content box, not
   the border box. */
body.gp-hero .gp-hd__link,
body.gp-hero .gp-hd__row,
body.gp-hero .gp-hd__link::before,
body.gp-hero .gp-hd__ig::before,
body.gp-hero .gp-hd__logo,
body.gp-hero .gp-hd__ig{
  transition:color 600ms var(--gp-motion-easing),
             border-color 600ms var(--gp-motion-easing),
             background-color 600ms var(--gp-motion-easing);
}
body.gp-hero .gp-hd__logo,
body.gp-hero .gp-hd__ig{
  background-color:var(--gp-rule);
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  -webkit-mask-position:center;  mask-position:center;
  -webkit-mask-origin:content-box; mask-origin:content-box;
}
body.gp-hero .gp-hd__logo{
  -webkit-mask-image:url('../images/Group-4330.svg'); mask-image:url('../images/Group-4330.svg');
  -webkit-mask-size:200px 38px; mask-size:200px 38px;
}
body.gp-hero .gp-hd__ig{
  -webkit-mask-image:url('../images/aca397ff.svg'); mask-image:url('../images/aca397ff.svg');
  -webkit-mask-size:21px auto; mask-size:21px auto;
}
/* The images keep their layout box so the row does not resize; the mask does
   the drawing. */
body.gp-hero .gp-hd__logo img,
body.gp-hero .gp-hd__ig img{visibility:hidden;}

html.gp-navy body.gp-hero .gp-hd__link,
html.gp-navy body.gp-hero .gp-hd__link[aria-current]{color:var(--gp-accent-navy);}
html.gp-navy body.gp-hero .gp-hd__row{border-bottom-color:var(--gp-accent-navy);}
html.gp-navy body.gp-hero .gp-hd__link::before,
html.gp-navy body.gp-hero .gp-hd__ig::before,
html.gp-navy body.gp-hero .gp-hd__logo,
html.gp-navy body.gp-hero .gp-hd__ig{background-color:var(--gp-accent-navy);}

/* The announcement bar slides up out of view on About and is simply present
   everywhere else, arriving with the header drop. 1fr to 0fr rather than a
   measured height: the bar is one line of text whose height depends on the
   font, and a hard-coded number would be wrong the first time that changes. */
body.gp-hero .gp-hd__bar{
  display:grid;grid-template-rows:1fr;
  transition:grid-template-rows 450ms var(--gp-motion-easing),
             opacity 300ms var(--gp-motion-easing);
}
/* The link's own 4px block padding is not part of the row height that 0fr
   collapses, so it has to go too — otherwise 8px of dead space survives and
   holds the header down by that much. */
body.gp-hero .gp-hd__bar > a{
  overflow:hidden;
  transition:padding-block 450ms var(--gp-motion-easing);
}
html.gp-nobar body.gp-hero .gp-hd__bar > a{padding-block:0;}
html.gp-nobar body.gp-hero .gp-hd__bar{grid-template-rows:0fr;opacity:0;}

/* Leaving, both have to finish inside the veil's 240ms hold or they would be
   caught mid-change on a navbar that sits above the veil at z-index 1000. */
html.gp-veil-in body.gp-hero .gp-hd__link,
html.gp-veil-in body.gp-hero .gp-hd__row,
html.gp-veil-in body.gp-hero .gp-hd__link::before,
html.gp-veil-in body.gp-hero .gp-hd__ig::before,
html.gp-veil-in body.gp-hero .gp-hd__logo,
html.gp-veil-in body.gp-hero .gp-hd__ig,
html.gp-veil-in body.gp-hero .gp-hd__bar,
html.gp-veil-in body.gp-hero .gp-hd__bar > a{transition-duration:220ms;}

.gp-hd__bar{background:var(--gp-maroon);text-align:center;}
.gp-hd__bar a{
  color:var(--gp-cream);text-decoration:none;font-size:.62rem;letter-spacing:var(--gp-tracking-normal);
  display:block;padding:4px;text-transform:uppercase;
  font-family:var(--gp-font-mono), sans-serif;
}
/* On Jobs the bar is not a link — see partials/navbar-site.html. Without this
   it keeps the pointer cursor and still looks clickable. */
.gp-hd__bar a:not([href]){cursor:default;}
/* Deliberately NOT .padding-global: that class is display:flex in
   gp-ecom.webflow.css, which would make the row a flex item sized to its
   content and stop the navbar halfway across the viewport. */
.gp-hd__pad{display:block;padding-left:75.5px;padding-right:75.5px;}

/* Cells stretch the full row height so every vertical divider meets the rule. */
.gp-hd__row{
  width:100%;display:flex;align-items:stretch;min-height:78px;
  padding-top:14px;padding-bottom:0;border-bottom:1px solid var(--gp-rule);
}
.gp-hd__logo{flex:0 0 auto;display:flex;align-items:center;line-height:0;padding-top:10px;}
.gp-hd__logo img{width:200px;height:38px;display:block;}
.gp-hd__links{margin-left:auto;display:flex;align-items:stretch;}
/* Labels sit just above the rule, bottom-aligned with the logo — not centred. */
.gp-hd__link{
  width:186px;display:flex;align-items:flex-end;padding:0 0 7.9px 18px;
  font-family:var(--gp-font-mono), sans-serif;font-size:12.8px;font-weight:400;letter-spacing:var(--gp-tracking-normal);
  line-height:1.1;color:var(--gp-brown);text-transform:uppercase;text-decoration:none;
  position:relative;white-space:nowrap;
}
/* Dividers: half the cell height, growing up from the rule so they touch it. */
.gp-hd__link::before,.gp-hd__ig::before{
  content:"";position:absolute;left:0;bottom:0;width:1px;height:50%;background:var(--gp-rule);
}
.gp-hd__link::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:2px;background:var(--gp-underline);
  transform:scaleX(0);transform-origin:left center;transition:transform .3s ease;
}
.gp-hd__link:hover::after,.gp-hd__link[aria-current]::after{transform:scaleX(1);}
.gp-hd__link[aria-current]{color:var(--gp-underline);}
.gp-hd__ig{
  flex:0 0 auto;width:60px;display:flex;align-items:flex-end;justify-content:center;
  position:relative;padding-bottom:7.5px;line-height:0;
}
.gp-hd__ig img{width:21px;height:auto;display:block;}

/* Clear the fixed header. --gp-hd-h is published by the partial's script, so it
   tracks the real height across breakpoints instead of a hard-coded guess.
   Applied to body rather than .page-wrapper because only About and Jobs have a
   .page-wrapper — Contact, Privacy and Terms do not.
   About is exempt: its hero is full-bleed and designed to run under a
   transparent navbar, exactly as the Webflow original did.

   Scoped with :has() to pages that actually HAVE a header to clear. Applied to
   every body it padded the splash too, which carries no navbar — so
   --gp-hd-h was never published, the 110px fallback applied, and a page that
   is exactly one screen tall scrolled by precisely that much. Keying on the
   header's presence makes any future headerless page right by default.

   🔴 The value is declared HERE as well as published by the script, and the two
   must agree. It used to be a 110px fallback inside the var(), which matched
   neither breakpoint: the header is 101px above 991px and 127px below. Every
   page therefore painted its first frame 9px too low and jumped up the instant
   the script ran — the "blink" on load. Locally the script runs inside the same
   parse-and-paint cycle so that frame is never shown; over a network the paint
   can land first, which is why it only ever appeared on the deployed site.
   Measured 3 Sep 2026 at 1400 / 900 / 600 / 420px. If the header's height
   changes, change these too — the script's value wins once it runs, so a stale
   number here shows up only as a flicker on first paint. */
:root{--gp-hd-h:101px;}
@media(max-width:991px){:root{--gp-hd-h:127px;}}
body:has(.gp-hd){padding-top:var(--gp-hd-h);}
body.gp-hero{padding-top:0;}

/* The front door is exactly one screen and does not scroll. dvh keeps it
   honest on mobile, where the URL bar makes vh larger than the visible area. */
/* The splash page carries the site footer as of 9 Sep 2026, OVERLAID on the
   photography rather than sitting below it. A cream bar under a full-bleed
   image cut the picture in half; transparent, the photograph runs to the
   bottom of the screen and the footer reads as part of it.
   The splash keeps its full 100dvh and the footer is absolutely positioned
   over its foot, so the page is still exactly one screen with no scroll —
   body.gp-splash keeps overflow:hidden. */
body.gp-splash{padding-top:0;overflow:hidden;position:relative;}
.splash{min-height:100dvh;}
body.gp-splash .gp-ft{
  position:absolute;left:0;right:0;bottom:0;z-index:3;
  background:transparent;
  /* The rule stays, as on every other page — but in the footer's own cream at
     low alpha, not var(--gp-rule). The page rule is a brown mixed for a cream
     ground and disappears against photography. */
  border-top:1px solid color-mix(in srgb, var(--gp-accent-cream) 38%, transparent);
  padding-top:18px;padding-bottom:22px;
}
/* About, Locations, Jobs and Contact are already the splash's own navigation,
   directly above this. Repeating them in the footer of the same screen is
   noise, so the home page footer carries only what the splash nav does not:
   the legal pages and Instagram. The links stay in the markup — the partial is
   shared with six other pages where all seven belong. */
body.gp-splash .gp-ft__links a:nth-child(-n+4){display:none;}
/* Privacy is now the first visible link, so it must not draw a divider against
   nothing on its left. */
body.gp-splash .gp-ft__links a:nth-child(5)::before{display:none;}
/* Cream on photography, with the same shadow the nav carries. Without it the
   footer is unreadable on the lighter interiors — measured on splash-03, the
   brightest of the six, when the nav had no shadow. */
body.gp-splash .gp-ft__links a,
body.gp-splash .gp-ft__note{
  color:var(--gp-accent-cream);
  text-shadow:0 1px 10px rgba(0,0,0,.55), 0 0 2px rgba(0,0,0,.45);
}
body.gp-splash .gp-ft__note{opacity:.9;}
/* The dividers take the text colour rather than the page rule, or they vanish
   against the photograph. Same treatment as .splash__nav a + a::before. */
body.gp-splash .gp-ft__links a + a::before{background:currentColor;opacity:.45;}
body.gp-splash .gp-ft__links a:hover{color:var(--gp-accent-cream);opacity:.72;}
/* Short viewports (landscape phones): shrink the content rather than let
   overflow:hidden clip the nav off the bottom. */
@media(max-height:560px){
  .splash__logo{width:min(400px,62vw);}
  .splash__nav{margin-top:34px;}
}

/* ------------------------------------------------------------------ locations
   Two stores side by side, each an illustrated sign over a details card.
   Follows the mockup in images/panel-locations.webp; the two illustrations were
   cropped out of that mockup to store-highgate.webp / store-westhampstead.webp. */
.loc{
  background:var(--gp-cream);min-height:100vh;
  /* 78px of air between the header and the first illustration. This was 156 —
     56 plus 100 of deliberate extra — and halved on 9 Sep 2026: with the
     illustrations at their current size the gap read as a missing element
     rather than as breathing room. */
  padding:78px 75.5px 90px;box-sizing:border-box;
  font-family:var(--gp-font-mono), sans-serif;color:var(--gp-brown);
}
/* "OUR STORES" is hidden VISUALLY but kept in the document.
   Removing the element outright would leave the page with no h1 at all, which
   costs the document outline that screen readers navigate by and the heading
   search engines read first. This is the standard visually-hidden pattern: off
   the screen for sighted users, still announced and still indexed.
   The store names remain the first visible headings. */
.loc__title,
.gp-sr-only,
/* Same treatment for the splash. The home page's meaning is the wordmark, which
   is an <img>, so it had no h1 at all until 8 Sep 2026 — caught by
   scripts/ci/check-a11y-seo.py on that gate's first run. */
.splash__title{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0;
}
/* The grid carried its spacing from the heading's margin, so it needs its own. */
.loc__grid{margin-top:8px;}
/* The centre divider. Back to a pseudo-element rather than a border on the
   first column, because a border is the height of its box and this one is
   inset top and bottom — it stops --gp-space-xxl short at each end instead of
   running the full grid height. Absolutely positioned, so it is not a grid
   item and claims no cell; position:relative on the grid is safe, as the only
   absolutely positioned thing inside it is .loc__link::before, which resolves
   against .loc__link's own position:relative.

   0.5px at 50% alpha: a true sub-pixel stroke on hidpi, with the alpha applied
   through color-mix so --gp-rule stays the single source for the hue. */
.loc__grid{display:grid;grid-template-columns:1fr 1fr;gap:0;max-width:1500px;margin:0 auto;position:relative;}
.loc__grid::before{
  content:"";position:absolute;left:50%;top:var(--gp-space-xxl);bottom:var(--gp-space-xxl);
  width:0.5px;transform:translateX(-50%);pointer-events:none;
  background:color-mix(in srgb, var(--gp-rule) 50%, transparent);
}
/* 0.5px has no sub-pixel to land on at 1x and can round away to nothing — an
   external display or projector would show no divider at all. A full pixel
   there; the 50% alpha still carries the lightness. */
@media (max-resolution: 1.5dppx){
  .loc__grid::before{width:1px;}
}
.loc__store{padding:0 52px;display:flex;flex-direction:column;}
/* Fixed box height, art bottom-aligned inside it. The two illustrations are
   different heights; without this the store names sit at different heights and
   the two columns stop reading as a pair. */
.loc__art{
  height:300px;width:auto;max-width:100%;display:block;margin:0 auto 26px;
  object-fit:contain;object-position:center bottom;align-self:center;
  transition:filter var(--gp-motion-fade) var(--gp-motion-easing);
}
/* Both PNGs are RGBA with a transparent ground, so brightness darkens the line
   work itself and not a rectangle behind it. The transition sits on .loc__art
   rather than on :hover, so it eases on the way out as well as in. */
.loc__store:hover .loc__art{filter:brightness(0.82);}
@media (prefers-reduced-motion: reduce){
  .loc__art{transition:none;}
}
/* ── The DIRECTIONS starburst ────────────────────────────────────────────────
   A retro price-sticker badge on the top-right corner of each store
   illustration. Chosen over an inline pill beside the address because the
   pill made the two mirrored store cards stop mirroring: it fitted beside
   "269 Archway Road" and wrapped below "280 West End Lane".

   .loc__artwrap is width:fit-content so it hugs the rendered image rather
   than the column. Anchoring to the column would have floated the badge far
   off the artwork, because the illustrations are ~340px wide inside a ~700px
   column. This is what makes the position hold at every breakpoint.

   Size is driven by ONE custom property so a breakpoint changes one number
   and the shape, text and offsets follow. The type stays at --gp-size-micro
   throughout — never a raw px value, or check-design.sh's scale ratchet
   fails, which is exactly what it is there for. */
.loc__artwrap{position:relative;width:fit-content;margin:0 auto;}
.loc__star{
  --gp-star:118px;
  /* Straddling the corner rather than sitting inside it: at top:0/right:0 the
     badge lands ON the artwork and covers the GROCERY POST lettering on the
     Highgate sign. The negative offsets are in em of the badge's own size, so
     they shrink with it at each breakpoint instead of needing their own rules. */
  position:absolute;top:-0.09em;right:-0.14em;z-index:2;
  font-size:var(--gp-star);
  width:var(--gp-star);height:var(--gp-star);
  display:flex;align-items:center;justify-content:center;
  text-align:center;text-decoration:none;
  background:var(--gp-maroon);color:var(--gp-cream);
  /* Every moving part is a custom property, so directions-magnet.js writes
     four numbers and never has to know the badge is a rotated star. With no
     JS all four stay at their defaults and this is a plain 9deg tilt. */
  --gp-star-x:0px; --gp-star-y:0px; --gp-star-scale:1; --gp-star-lean:0deg;
  transform:translate3d(var(--gp-star-x),var(--gp-star-y),0)
            rotate(calc(9deg + var(--gp-star-lean)))
            scale(var(--gp-star-scale));
  transform-origin:center;
  transition:transform var(--gp-motion-fade) var(--gp-motion-easing);
  will-change:transform;
  /* 12 spikes, not 16. At 16 the points were fine enough to read as a scallop
     at 88px on a phone. Fewer, deeper spikes (inner radius 36% rather than
     39%) hold the price-sticker silhouette down to the smallest size. */
  clip-path:polygon(
    50.0% 0.0%, 59.3% 15.2%, 75.0% 6.7%, 75.5% 24.5%, 93.3% 25.0%,
    84.8% 40.7%, 100.0% 50.0%, 84.8% 59.3%, 93.3% 75.0%, 75.5% 75.5%,
    75.0% 93.3%, 59.3% 84.8%, 50.0% 100.0%, 40.7% 84.8%, 25.0% 93.3%,
    24.5% 75.5%, 6.7% 75.0%, 15.2% 59.3%, 0.0% 50.0%, 15.2% 40.7%,
    6.7% 25.0%, 24.5% 24.5%, 25.0% 6.7%, 40.7% 15.2%
  );
}
.loc__star__txt{
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-micro);
  letter-spacing:var(--gp-tracking-normal);text-transform:uppercase;
  line-height:1.1;max-width:66%;
}
.loc__star:hover,.loc__star:focus-visible{--gp-star-scale:1.07;}
/* While the magnet is driving, the CSS transition would lag one frame behind
   every rAF write and turn the lean into a smear. JS owns the easing then. */
.loc__star.is-magnetic{transition:none;}
/* clip-path clips the focus ring with the shape, so the badge would have no
   visible focus state at all. An offset outline on the wrapper is the only
   ring that survives the clip. */
.loc__star:focus-visible{outline:none;}
.loc__artwrap:has(.loc__star:focus-visible){
  outline:2px solid var(--gp-underline);outline-offset:4px;
}
@media (prefers-reduced-motion: reduce){
  .loc__star{transition:none;}
  .loc__star:hover,.loc__star:focus-visible{--gp-star-scale:1;}
}
/* One number per breakpoint. Below 992px the columns stack and the artwork
   grows relative to the viewport, so the badge shrinks to stay a badge. */
/* The tucked-in offsets start as soon as the columns stack, not at 479. The
   clipping gate caught the badge 8px past the right edge at 480px: below 991
   the artwork grows relative to the viewport, so the outward straddle that
   looks right on a desktop pushes the badge off a narrow screen. */
@media screen and (max-width:991px){
  .loc__star{--gp-star:104px;top:-0.03em;right:-0.03em;}
}
/* Below 480 the badge stops straddling and tucks into the corner. Measured at
   375px: the artwork is 341px wide with ~17px of gutter left, while the badge
   needs the outward offset PLUS the extra its 11deg rotation adds to the
   bounding box — about 21px. It overhung the viewport by 4px and html/body
   carry overflow-x:clip, so it was silently CUT rather than causing a
   scrollbar. Nothing would have reported it. */
@media screen and (max-width:479px){ .loc__star{--gp-star:88px;} }
/* Store names. One step down the scale and regular weight rather than bold —
   the h1 above them is now visually hidden, so nothing needs out-ranking, and
   the uppercase plus wider letter-spacing gives them presence without weight. */
/* --gp-size-label, one step below the section-heading step. That puts it
   adjacent to the caption step the fact labels use, which the hierarchy rule
   normally forbids — so the rank is carried by tracking (widest against
   wider), colour (ink against muted brown) and position (centred and alone)
   rather than by size. Recorded as a deliberate exception in the design guide,
   not an oversight. */
.loc__name{
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-label);font-weight:400;letter-spacing:var(--gp-tracking-normal);
  text-transform:uppercase;color:var(--gp-brown);text-align:center;margin:0 0 22px;
}
/* No border or radius on the info block — the label column and the tracking
   carry the structure. Padding is kept so it does not sit flush against the
   store name above it.

   Centred on the illustration above it rather than filling the column. The
   50px indent this replaces was pushing the facts 26px right of the art's
   centre line — the card box itself was already centred, so the indent was the
   whole of the offset. fit-content lets the two fact tracks collapse to their
   own content, which lands the block at roughly the width of the artwork
   instead of half again as wide. */
.loc__card{padding:26px 0 30px;flex:1;width:fit-content;max-width:100%;align-self:center;}
/* Three columns: label, value, action. The value column is max-content, so it
   sizes to the longest value on the page and every pill therefore starts at the
   same x — EMAIL and CALL stack neatly — while still sitting one gap away from
   its own value rather than out at the edge of the card. The pill rows opt into
   the parent's tracks with subgrid, which is what lets the alignment be derived
   instead of hard-coded to a width that would drift the moment a name changes. */
/* Two columns of facts under each store: address and opening hours on the
   first row, manager and telephone on the second. Each fact is its own block
   with the label above its value — which is what makes two columns fit at all.
   A label beside its value wants about 240px ("OPENING HOURS" plus the longest
   hours line) and the half-column is 250px, so it would have had nothing in
   hand the first time a manager's name or a label got longer. */
.loc__facts{margin:0;display:grid;grid-template-columns:1fr 1fr;gap:var(--gp-space-lg) var(--gp-space-xl);}
.loc__fact{display:flex;flex-direction:column;gap:var(--gp-space-xs);min-width:0;}
/* Opening hours and telephone pushed 50px further from the address and manager
   beside them. Applied as padding on the second column's facts rather than a
   wider column-gap, so it stays expressible when the grid drops to one column
   — where it is switched off again, since there is no second column to shift. */
.loc__facts > :nth-child(even){padding-left:50px;}
/* A fact with no partner spans both columns rather than leaving a hole beside
   it, and drops the 50px it would otherwise inherit from the even-child rule
   when it happens to land in that position. */
.loc__fact--wide{grid-column:1 / -1;padding-left:0 !important;}
.loc__facts dt{
  font-size:var(--gp-size-caption);letter-spacing:var(--gp-tracking-wider);text-transform:uppercase;
  color:var(--gp-brown);opacity:.75;white-space:nowrap;
}
/* Values sit at the same step as their labels and in the same case, so a fact
   reads as one unit rather than a small label above larger prose. */
.loc__facts dd{
  margin:0;font-size:var(--gp-size-caption);line-height:1.6;
  text-transform:uppercase;letter-spacing:var(--gp-tracking-wide);color:var(--gp-ink);
}
/* Small inline actions beside a fact — mailto and tel. */
.loc__action{
  display:inline-block;font-size:var(--gp-size-micro);letter-spacing:var(--gp-tracking-wider);
  text-transform:uppercase;color:var(--gp-underline);text-decoration:none;
  border:1px solid currentColor;border-radius:var(--gp-radius-action);padding:1px 6px;
  vertical-align:1px;white-space:nowrap;
}
/* The pill follows its own value inline. EMAIL and CALL now share a grid row
   rather than sitting one above the other, so they line up horizontally by
   construction and need no alignment rule of their own. */
.loc__facts dd:has(.loc__action){display:flex;align-items:center;gap:var(--gp-space-sm);}
.loc__action:hover{border-color:var(--gp-underline);}
/* Unfilled content is marked, not faked. Anything still bracketed is copy
   Grocery Post has to supply — never invent an address, phone or opening time
   for a real storefront. */
.loc [data-slot]{color:var(--gp-brown);opacity:.55;font-style:normal;}
.loc__link{
  display:inline-block;margin-top:24px;font-size:var(--gp-size-caption);letter-spacing:var(--gp-tracking-wider);
  text-transform:uppercase;color:var(--gp-underline);text-decoration:none;
  border-bottom:1px solid var(--gp-underline);padding-bottom:2px;
}
.loc__link:hover{opacity:.65;}
.loc__foot{
  text-align:center;margin:56px 0 0;
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-caption);
  letter-spacing:var(--gp-tracking-normal);text-transform:uppercase;color:var(--gp-brown);
}
.loc__foot a{color:var(--gp-underline);text-decoration:none;}

/* ── About's secondary nav, temporarily off ─────────────────────────────────
   The scroll progress bar — the strip of section links (our story, cult
   groceries, corner shop deluxe, postcard, merchandise, join us) that drops
   down as you scroll About, with the red progress rule under it.

   Turned off 8 Sep 2026 at James's request, TEMPORARILY. Delete this rule to
   bring it back; nothing else was changed.

   Hidden, not removed. The element carries data-w-id and is driven by IX2,
   which also sets its position — so deleting the markup would break the gate
   counts, and unpicking the interaction is not reversible. display:none costs
   nothing and undoes cleanly.

   Note it only works at all because <main> no longer keeps a transform after
   its enter animation (see the `backwards` fill on html[data-enter] main): a
   transformed ancestor makes this position:fixed element scroll away. If it
   comes back and is stranded off-screen, that is the cause. */
.nav_progressbar{display:none !important;}

/* ── Privacy and Terms ──────────────────────────────────────────────────────
   Both pages were Webflow's .w-richtext defaults: Garamond h1 in the hairline
   grey, system-ish h3s, and 14.4px body. On the role table these are a page
   title, section headings and body copy, so they take those steps.

   The title matches Contact's — mono, uppercase, --gp-size-body — so the two
   utility pages read as the same kind of page. Headings and body share
   --gp-size-label, which the hierarchy rule would normally forbid; the rank is
   carried by family (mono against Suisse), case, colour and a hairline above
   each heading, exactly as the store names do it on Locations. Sized down from
   body/16 because these are long documents and 16px mono uppercase headings in
   a wall of prose shout. */
.gp-legal__title{
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-body);
  font-weight:400;line-height:1.2;letter-spacing:var(--gp-tracking-normal);
  text-transform:uppercase;color:var(--gp-ink);margin:0 0 var(--gp-space-md);
}
.simple-page .gp-legal__intro{
  margin:0 0 var(--gp-space-xl);max-width:60ch;
  font-family:var(--gp-font-body), sans-serif;font-size:var(--gp-size-label);
  line-height:1.6;color:var(--gp-brown);
}
.gp-legal__body{max-width:68ch;}
.simple-page .gp-legal__body h2{
  margin:var(--gp-space-xl) 0 var(--gp-space-sm);padding-top:var(--gp-space-md);
  border-top:1px solid var(--gp-rule);
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-label);
  font-weight:400;letter-spacing:var(--gp-tracking-normal);
  text-transform:uppercase;color:var(--gp-ink);
}
.simple-page .gp-legal__body h2:first-child{margin-top:0;padding-top:0;border-top:0;}
.simple-page .gp-legal__body p{
  margin:0 0 var(--gp-space-md);
  font-family:var(--gp-font-body), sans-serif;font-size:var(--gp-size-label);
  line-height:1.7;color:var(--gp-ink);
}
.simple-page .gp-legal__body a{
  color:var(--gp-underline);text-decoration:none;
  border-bottom:1px solid currentColor;
}
.simple-page .gp-legal__body a:hover{opacity:.65;}
/* The footer's own small-screen rule carried a .66rem literal, the only size on
   this page that was not a step. Down to the micro step it was approximating. */

/* ── Jobs: the one type size that is free to move ───────────────────────────
   Audited 3 Sep 2026. The Jobs page carries 13 distinct type treatments and 10
   of them are off the ten-step scale, but almost all of those are fitted type,
   not drift: .apply-today (top:-365px), .tellusabit (absolute, left:69px,
   width:200px), .livingwage (absolute, width:133px, text-align:justify) and
   .showmethemoney are sized to sit inside joinus-macwindow.svg, and .freebumbag
   is rotated -11deg onto the bumbag photograph. Changing any of their sizes
   breaks the fit — .showmethemoney is a documented hard rule in CLAUDE.md for
   exactly this reason, and the other four are the same thing undocumented.
   The two poster headings, 104px and 48px, are the design.

   Two rules on that page ARE free to move, both ordinary flowed text with
   nothing but a bottom margin, and both at 1.2rem — the largest things on the
   page with no reason to be. They are separate rules that happened to share a
   value: .jobs-board_availableroles sets "AVAILABLE ROLES", and
   .info-block_job-title sets each job's <h3>. They move together, or the
   section heading ends up smaller than the items listed beneath it.

   Down one step to --gp-size-body, and their -.05em onto --gp-tracking-tighter,
   which is that value exactly.

   Overridden here rather than edited in gp-ecom.webflow.css: the monolith is
   migrated as a tracked exercise, not touched in passing, and site.css is last
   in the cascade so a plain class selector is enough. */
.jobs-board_availableroles,
.info-block_job-title{
  font-size:var(--gp-size-body);
  letter-spacing:var(--gp-tracking-tighter);
}

/* ── Contact form ───────────────────────────────────────────────────────────
   The Webflow form carried three fonts (Garamond heading, system labels and
   button, Suisse inputs), three different blacks for its borders (#272727 on
   the inputs, #3d3d3d on the textarea, #000 on the button), two radii, and
   name/email boxes 671px wide against a 736px textarea, so the right edge was
   ragged. All of that came from .formfield / .formlabel / .formbutton /
   .w-input / .w-button, which the markup no longer carries.

   Rebuilt on the same vocabulary as Locations: mono uppercase labels in the
   muted brown, hairline underlines rather than boxes — the site's rules,
   dividers and link underlines are all hairlines, so a boxed field was the
   odd one out — and the submit styled as a larger sibling of the EMAIL/CALL
   pills. Every value is a token. */
/* PAGE TITLE role, MONO ladder — see the role table in design-guide.md.
   --gp-size-body, still a step above the section-heading role the Locations
   store names use, so the rank reads. The mono face has its own ceiling: it
   never takes --gp-size-title or above anywhere on the site, and
   scripts/check-design.sh fails the build if it does.

   Tracking is --gp-tracking-wider, not widest. Widest is sized for the store
   names, which are a short word standing alone; at this length and this size it
   spaced the title out until it stopped reading as one phrase. Wider is the
   step the nav and buttons use, which is what this is closer to.

   A Garamond page title takes display/56 for the same rank — uppercase mono is
   optically far heavier at any given px, so the two faces need different
   numbers to sit at the same level. */
/* Same step as .gp-form__alt at the foot of the form. At body it was the
   largest thing on a page whose whole voice is one quiet mono size, and it
   shouted. */
.gp-form__title{
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-label);
  font-weight:400;line-height:1.2;letter-spacing:var(--gp-tracking-normal);
  text-transform:uppercase;color:var(--gp-ink);margin:0 0 var(--gp-space-sm);
}
/* Scoped to .simple-page to match the specificity of `.simple-page p` in
   gp-ecom.webflow.css (0,2,1), which sets font-size 1.0625rem and
   margin-bottom 1rem. A bare .gp-form__intro loses that contest and silently
   renders at 17px. Ties are broken by source order, and site.css is last. */
/* STANDFIRST role, not lead paragraph — one line under a title on a utility
   page, rather than an opening paragraph of prose. That is a role change, so
   the step follows it down from body-large to label. */
/* Mono, like everything else on this page. It was the only element in Suisse —
   the title, the field labels, the submit and the "or email us directly" line
   are all mono — so it read as a stray voice under its own heading.
   The tracking comes with it: mono in this system is never set at `normal`,
   because the face is already wide and the letterfit looks cramped without it.
   Every other mono element here carries between 0.5 and 1.6px. */
.simple-page .gp-form__intro{
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-caption);
  letter-spacing:var(--gp-tracking-normal);text-transform:uppercase;
  line-height:1.6;color:var(--gp-brown);margin:0 0 var(--gp-space-xl);max-width:34em;
}
/* Name and email share a row; the message spans both. One column once there is
   no longer room for two comfortable fields. */
.gp-form{max-width:720px;}
.gp-form__grid{
  display:grid;grid-template-columns:1fr 1fr;
  gap:var(--gp-space-lg) var(--gp-space-xl);align-items:end;
}
.gp-form__field{display:flex;flex-direction:column;gap:var(--gp-space-sm);min-width:0;}
.gp-form__field--wide{grid-column:1 / -1;}
/* Same treatment as a Locations label, so the two pages read as one system. */
.gp-form label{
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-caption);
  letter-spacing:var(--gp-tracking-normal);text-transform:uppercase;color:var(--gp-brown);
}
/* 16px is not a style choice: below it, iOS zooms the page on focus. */
/* The BODY face, not mono, and this follows from the mono rule rather than
   breaking it. Mono is only ever set in uppercase here — but this is the one
   place on the site where the text is typed by the visitor, and uppercasing it
   means somebody cannot read back the email address they just entered.
   text-transform is presentational so the submitted value would still be
   correct, which makes it worse: it looks wrong and is right, and the person
   filling the form has no way to tell.
   So the field takes the body face. The LABEL above it stays mono and
   uppercase, which is what carries the form's voice. */
.gp-form input[type=text],.gp-form input[type=email],.gp-form textarea{
  font-family:var(--gp-font-body), sans-serif;font-size:var(--gp-size-body);
  letter-spacing:var(--gp-tracking-normal);color:var(--gp-ink);
  background:transparent;border:0;border-bottom:1px solid var(--gp-rule);
  border-radius:var(--gp-radius-none);padding:var(--gp-space-sm) 0;
  width:100%;min-width:0;-webkit-appearance:none;appearance:none;
  transition:border-color var(--gp-motion-fade) var(--gp-motion-easing);
}
.gp-form textarea{resize:vertical;min-height:150px;line-height:1.6;}
/* The red underline is the focus indicator; box-shadow rather than a wider
   border so nothing below it shifts by a pixel when the field is focused. */
.gp-form input:focus,.gp-form textarea:focus{
  outline:none;border-bottom-color:var(--gp-underline);
  box-shadow:0 1px 0 0 var(--gp-underline);
}
.gp-form input::placeholder,.gp-form textarea::placeholder{color:var(--gp-brown);opacity:.55;}
/* A larger sibling of the EMAIL/CALL pills — same border-on-currentColor
   construction, so it can never drift away from the text it surrounds. */
.gp-form__submit{
  grid-column:1 / -1;justify-self:start;margin-top:var(--gp-space-sm);
  /* --gp-size-label IS the guide's step for a button, and the navbar links sit
     at the same one — this is not a leftover. The bulk was the box: it was on
     the large height and padding (48/24) where the guide's default button is
     38/15. */
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-label);
  letter-spacing:var(--gp-tracking-normal);text-transform:uppercase;
  color:var(--gp-underline);background:transparent;
  border:1px solid currentColor;border-radius:var(--gp-radius-action);
  height:var(--gp-btn-height-md);padding:0 var(--gp-btn-pad-xmd);cursor:pointer;
  -webkit-appearance:none;appearance:none;
  transition:background-color var(--gp-motion-fade) var(--gp-motion-easing),
             color var(--gp-motion-fade) var(--gp-motion-easing);
}
.gp-form__submit:hover{background:var(--gp-underline);color:var(--gp-text-on-dark);}
.gp-form__submit:focus-visible{outline:2px solid var(--gp-underline);outline-offset:3px;}
/* Webflow toggles display on these, so nothing here may set display. */
.gp-form__note{
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-caption);
  letter-spacing:var(--gp-tracking-normal);text-transform:uppercase;
  color:var(--gp-brown);background:transparent;border:0;
  padding:var(--gp-space-md) 0 0;margin:0;
}
.gp-form__note--fail{color:var(--gp-underline);}
/* Centred, and at the same rank as the Locations closing line — the two are
   the same thing on two pages, so they take the same treatment. A step up from
   caption because this is a sentence, not a field label; at caption size it was
   competing with the form's own labels for the same rank. */
/* Caption, matching .gp-form__intro at the head of the form. The two are the
   same kind of line — a quiet aside either side of the fields — so they take
   the same step, one below the title. */
.simple-page .gp-form__alt{
  margin:var(--gp-space-xxl) 0;text-align:center;
  font-family:var(--gp-font-mono), sans-serif;
  font-size:var(--gp-size-caption);letter-spacing:var(--gp-tracking-normal);
  text-transform:uppercase;color:var(--gp-brown);
}
.gp-form__alt a{color:var(--gp-underline);text-decoration:none;border-bottom:1px solid currentColor;}
.gp-form__alt a:hover{opacity:.65;}

@media(max-width:1200px){
  .gp-hd__pad{padding-left:40px;padding-right:40px;}
  /* 130 and 46, not 140 and 60: at 992px the row has 912px of usable width and
     needs logo 200 + 5 links + the Instagram cell. At the wider values that is
     960px, so the Instagram cell hung ~9px past the right edge. */
  .gp-hd__link{width:130px;padding-left:14px;}
  .gp-hd__ig{width:46px;}
  .loc{padding-left:40px;padding-right:40px;}
  .loc__store{padding:0 32px;}
}
/* The navbar stacks from 991px down, NOT 640px — 991 is Webflow's own
   tablet breakpoint, so the two agree. The one-row layout needs
   logo 160 + five links + the Instagram cell, and the link labels are
   white-space:nowrap so they cannot shrink to fit. Measured at 700px wide the
   links reached 748px and the Instagram cell 794px — running off the right
   edge, which is what "CONTACT" being cut to "C" was. */
@media(max-width:991px){
  .gp-hd__pad{padding-left:28px;padding-right:28px;}
  .gp-hd__row{flex-wrap:wrap;align-items:center;justify-content:center;padding-top:12px;padding-bottom:12px;}
  .gp-hd__logo{flex:0 0 100%;justify-content:center;padding-bottom:8px;}
  .gp-hd__logo img{width:160px;margin:0 auto;}
  .gp-hd__links{flex:0 0 100%;margin-left:0;justify-content:center;flex-wrap:wrap;}
  .gp-hd__link{width:auto;padding:5px 12px;font-size:11.5px;}
  .gp-hd__link::before{height:70%;}
  .gp-hd__link:first-child::before{display:none;}
  .gp-hd__link::after{display:none;}
  .gp-hd__ig{display:none;}
  /* one store per row — side by side gets too cramped to read */
  .loc{padding-left:28px;padding-right:28px;}
  .loc__grid{grid-template-columns:1fr;}
  /* The 300px fixed height exists ONLY to bottom-align two illustrations of
     different heights so the store names below them line up. Once the columns
     stack there is no pair to align, and the height becomes a letterbox: the
     artwork sits at the bottom of a 300px box with dead space above it. That
     dead space is where the DIRECTIONS badge was landing, floating clear of
     the drawing it is supposed to be stuck to. */
  .loc__art{height:auto;}
  .loc__store{padding:0 0 44px;}
  /* Stacked: the divider turns and becomes the rule between the two stores. */
  /* The junction between stacked stores is not the grid midpoint, so the
     pseudo-element cannot find it. Same stroke, inset the same way, drawn as a
     centred band on the bottom edge of the first store. */
  .loc__grid::before{display:none;}
  .loc__store:first-child{
    margin-bottom:44px;
    background:linear-gradient(color-mix(in srgb, var(--gp-rule) 50%, transparent) 0 0) no-repeat center bottom / 60% 0.5px;
  }
}
@media(max-width:640px){
  .splash__nav{margin-top:56px;left:0;}
  .splash__nav a{font-size:.78rem;padding:6px 13px;}

  /* stacking now happens at 900px; this block only tightens it further */
  .gp-hd__pad{padding-left:14px;padding-right:14px;}
  .gp-hd__logo img{width:150px;}
  .loc{padding-left:14px;padding-right:14px;}
  .loc__card{padding:20px 10px 24px;}
  .gp-form__grid{grid-template-columns:1fr;}
  .gp-form__submit{width:100%;justify-self:stretch;}
  /* Two columns are kept here rather than collapsing to one. What it costs is
     measured: the widest cell is Manager — "Freddie Farrel" at 101px, an 8px
     gap and a 50px EMAIL pill, so 159px — and Opening hours needs 144px for
     "Mon-Fri: 7am - 9pm" on one line. Two of those plus the column gap and the
     card and page padding come to roughly 375px of viewport, so the pair holds
     down to about there. Card padding drops 18px -> 10px a side purely to buy
     that room; the 50px indent on the right-hand column goes for the same
     reason. */
  .loc__facts{grid-template-columns:1fr 1fr;gap:var(--gp-space-lg) var(--gp-space-md);}
  .loc__facts > :nth-child(even){padding-left:0;}
  /* Below ~375px the pill no longer fits beside the name. Allowing the row to
     wrap drops it onto its own line intact, instead of the flex default, which
     keeps it alongside and squeezes the name into two ragged lines. */
  .loc__facts dd:has(.loc__action){flex-wrap:wrap;}
  .loc__card{width:auto;align-self:stretch;}
}

/* --------------------------------------------------------------------- footer
   For pages built fresh in the redesign. The five Webflow-derived pages keep
   their own footer — see partials/site-footer.html for why. */
.gp-ft{
  background:var(--gp-cream);border-top:1px solid var(--gp-rule);
  padding:34px 75.5px 44px;font-family:var(--gp-font-mono), sans-serif;
}
.gp-ft__links{display:flex;flex-wrap:wrap;justify-content:center;gap:0;}
.gp-ft__links a{
  color:var(--gp-brown);text-decoration:none;text-transform:uppercase;
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-caption);
  letter-spacing:var(--gp-tracking-normal);padding:6px 18px;position:relative;
}
.gp-ft__links a + a::before{
  content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);
  height:1.05em;width:1px;background:var(--gp-rule);
}
.gp-ft__links a:hover{color:var(--gp-underline);}
.gp-ft__note{
  text-align:center;margin:18px 0 0;text-transform:uppercase;
  font-family:var(--gp-font-mono), sans-serif;font-size:var(--gp-size-micro);
  letter-spacing:var(--gp-tracking-normal);color:var(--gp-brown);opacity:.7;
}
/* Two balanced rows once the seven links stop fitting on one.
   Left to flex-wrap they broke 6 + 1, dropping Instagram onto a line of its
   own. `flex:0 0 25%` puts exactly four on the first row; the remaining three
   are 75% of the width and `justify-content:center` centres them under it, so
   the two rows read as a block rather than a ragged wrap.
   The break is at 900px, not 640: measured, the natural wrap happens at ~760px,
   and a rule that starts below the problem never runs when it is needed. */
@media(max-width:900px){
  .gp-ft__links a{flex:0 0 25%;text-align:center;}
  /* The divider is drawn on the left edge of every link after the first. On a
     wrapped row the first item would carry one against nothing, so the 5th —
     first of the second row — drops it. */
  .gp-ft__links a:nth-child(5)::before{display:none;}
}
@media(max-width:640px){
  .gp-ft{padding-left:14px;padding-right:14px;}
  .gp-ft__links a{padding:6px 4px;font-size:var(--gp-size-micro);}
}

/* ============================================================ page transition
   Cross-document View Transitions. The browser animates a real navigation;
   js/gp-view-transitions.js only sets data-vt on <html> to say which way.

   Rebuilt from scratch on 2 Sep 2026 after three patches failed to stop a
   white flash between pages. Three decisions, each load-bearing:

   1. ONLY THE OUTGOING PAGE MOVES. ::view-transition-new(root) is a SNAPSHOT,
      and a snapshot is transparent wherever it has nothing to paint. Animating
      it slides an empty rectangle in whenever the incoming document has not
      painted yet. Unanimated, it is simply the live document, revealed as the
      old page slides off it.

   2. THE INCOMING PAGE IS RENDER-BLOCKED until <main id="gp-main"> is parsed;
      the link is in partials/head-vt.html. MEASURED: this browser does not
      support rel="expect", so that link is currently INERT and decision (1)
      above is what actually stops the flash here. Kept because it is correct
      and will help in engines that do ship it. The symptom that pointed at
      paint timing in the first place: the flash appeared only on the FIRST
      navigation to a page and stopped afterwards — a cold-cache signature,
      since a cached page paints before the transition begins.

   3. THE OUTGOING SNAPSHOT IS RAISED so it slides OVER the incoming page; by
      default the new side paints on top and would hide the slide entirely. If
      a browser ignores that z-index the effect degrades to an instant swap —
      no animation, but no flash either, which is the right way round to fail.

   The header deliberately has NO view-transition-name — see below. */
/* ⛔ TRANSITION OFF — one line, deliberately reversible.
   Re-enable by uncommenting the @view-transition rule below. Everything else
   in this block (keyframes, direction rules, js/gp-view-transitions.js, the
   render-blocking anchor) is intact and inert without it.

   Turned off 3 Sep 2026 at 01:40, the night before a demo, after the curved
   top-left corner proved to be inherent rather than fixable. A view transition
   renders the page as a COMPOSITED SNAPSHOT for its duration, and a composited
   layer receives the browser's rounded content-area corner applied to itself
   rather than clipping the page invisibly behind it. So the corner curves
   while the snapshot exists and snaps square when it is discarded — a flash on
   every navigation. Removing view-transition-name from the header fixed that
   element's copy of the problem but not the root's, because the root snapshot
   is a composited layer too. No CSS removes it while a transition is running.

   The remaining options were: accept the flash, or drop the transition.
   Dropped. Navigation is now instant, which is not a downgrade for a five-page
   site. Revisit with a same-document transition, which does not snapshot the
   whole root, when there is time to do it properly. */
/* @view-transition{navigation:auto;} */

/* ------------------------------------------------------- page carousel
   A directional slide, both ways, animating the page's own <main>.

   NOT the View Transitions API. That renders the page as a composited
   SNAPSHOT, and a composited layer picks up the browser's rounded
   content-area corner itself rather than being clipped invisibly behind it —
   so the top corner curved for the duration and snapped square after. Nothing
   in CSS switches that off. <main> is ordinary DOM and has none of it.

   Two halves, joined by js/gp-view-transitions.js:
     leaving   .gp-out-left / .gp-out-right on <html> — the click is held,
               main slides off, then navigation proceeds.
     entering  data-enter="right" / "left" on <html> — main slides in from
               the side it should logically come from.

   Durations MUST stay in step with OUT_MS in that script.

   overflow-x is clipped on both html and body while a page is leaving, and on
   entry. Sliding right creates real scrollable overflow — a transform to the
   right extends the scroll area in LTR, though one to the left does not — and
   without this a horizontal scrollbar flashes for the length of the animation.
   It is scoped to the animating state so the rest of the time the document is
   untouched. */
@keyframes gpEnterRight { from { transform:translateX(100%);  } to { transform:none; } }
@keyframes gpEnterLeft  { from { transform:translateX(-100%); } to { transform:none; } }
@keyframes gpLeaveLeft  { from { transform:none; } to { transform:translateX(-100%); } }
@keyframes gpLeaveRight { from { transform:none; } to { transform:translateX(100%);  } }

/* `backwards`, NOT `both`. A forwards fill leaves a transform applied to <main>
   for the life of the page, and an ancestor with a transform becomes the
   containing block for its position:fixed descendants. About's scroll
   progress bar is fixed and lives inside <main>: under `both` it stopped being
   fixed, scrolled away with the page, and — never entering the viewport — its
   IX2 scroll trigger never fired either, so the bar simply vanished from the
   page. Backwards fills only before the animation starts, and the end state is
   no transform, so <main> stops being a containing block once it lands. */
html[data-enter] main            { animation:gpEnterRight 420ms cubic-bezier(.22,.61,.36,1) backwards; }
html[data-enter="left"] main     { animation-name:gpEnterLeft; }

html.gp-out-left  main           { animation:gpLeaveLeft  340ms cubic-bezier(.55,.06,.24,.98) both; }
html.gp-out-right main           { animation:gpLeaveRight 340ms cubic-bezier(.55,.06,.24,.98) both; }

/* ── About's hero veil ──────────────────────────────────────────────────────
   About is the only page whose first screen is a full-bleed photograph rather
   than the cream ground, so arriving on it lands hard. A cream veil covers the
   page, then clears once the slide has landed: the page slides in as cream and
   the photograph resolves behind it. Leaving runs it backwards — the veil comes
   up first and the slide carries a cream page out.

   Drawn as a pseudo-element on body, so no element in the hero is touched.
   That matters: .section-hero and .skybg both carry data-w-id and are animated
   by IX2, and a second opacity animation on either would fight it.

   z-index 900 keeps it under the navbar at 1000, so the navbar behaves exactly
   as it does on every other page. position:fixed means the slide moves <main>
   out from underneath the veil rather than dragging it along. */
body.gp-hero::after{
  content:"";position:fixed;inset:0;background:var(--gp-cream);
  pointer-events:none;z-index:900;opacity:0;
}
/* `both` matters on arrival: the backwards fill holds the veil opaque through
   the delay, so there is no frame of bare photograph before the fade begins. */
html[data-enter] body.gp-hero::after{
  animation:gpVeilOut 520ms var(--gp-motion-easing) 420ms both;
}
html.gp-veil-in body.gp-hero::after{
  animation:gpVeilIn 240ms var(--gp-motion-easing) both;
}
/* The splash gets the same veil on the way out, for the same reason About has
   one: its first screen is a photograph, every other page's is cream, and
   sliding a photograph off to reveal cream is the jarring part. Washing to
   cream first means the slide carries a cream page out to meet a cream page
   coming in.
   NOT on the way to About, which is the one destination that is also a
   photograph — veiling to cream only to fade a second photograph up from cream
   is a wash the visitor did not need, and About already runs its own arrival.
   The veil covers the logo and nav as well as the background, which is
   deliberate: they are cream on photography and would otherwise be left
   floating invisibly on a cream field mid-transition. */
body.gp-splash::after{
  content:"";position:fixed;inset:0;background:var(--gp-cream);
  pointer-events:none;z-index:900;opacity:0;
}
html.gp-veil-in body.gp-splash::after{
  animation:gpVeilIn 240ms var(--gp-motion-easing) both;
}
@keyframes gpVeilOut { from { opacity:1; } to { opacity:0; } }
@keyframes gpVeilIn  { from { opacity:0; } to { opacity:1; } }

html[data-enter], html[data-enter] body,
html.gp-out-left, html.gp-out-left body,
html.gp-out-right, html.gp-out-right body { overflow-x:clip; }

/* The splash fades its own logo and nav in, so sliding it IN as well would
   double-animate the front door. Scoped to the enter case only — the earlier
   unscoped `body.gp-splash main{animation:none}` sat after the gp-out rules and
   killed the leave animation too, which is not what its comment claimed. The
   cost was two bugs at once: home did not slide away, and because animationend
   never fires on an element that is not animating, the script fell through to
   its OUT_MS + 90 backstop, so the page sat still for 430ms and then vanished.
   Leaving home slides, as it always should have.

   The :not() pair is load-bearing, not belt and braces. data-enter is set on
   every page load and never removed, so html[data-enter] still matches while
   the page is leaving — scoping on that attribute alone suppressed the leave
   exactly as the unscoped rule did. The out classes are what actually
   distinguish the two directions. */
html[data-enter]:not(.gp-out-left):not(.gp-out-right) body.gp-splash main { animation:none; }

@media(prefers-reduced-motion:reduce){
  html[data-enter] main, html.gp-out-left main, html.gp-out-right main { animation:none; }
  /* The veil is motion too — no fade, and the JS skips its hold to match. */
  body.gp-hero::after,
  html[data-enter] body.gp-hero::after,
  html.gp-veil-in body.gp-hero::after { animation:none; opacity:0; }
  html[data-nav-in] .gp-hd { animation:none; }
  /* No navy fade and no bar collapse; the script skips their scheduling too. */
  body.gp-hero .gp-hd__link, body.gp-hero .gp-hd__row,
  body.gp-hero .gp-hd__logo, body.gp-hero .gp-hd__ig,
  body.gp-hero .gp-hd__bar { transition:none; }
}

@keyframes gpSlideOutLeft { to { transform:translateX(-100%); } }
@keyframes gpSlideOutRight{ to { transform:translateX(100%);  } }

/* Pin the root group. The UA animation on ::view-transition-group(root)
   interpolates the group's SIZE and POSITION between the old and new
   snapshots. Those differ whenever the two pages disagree about scroll extent
   — the splash does not scroll at all, About is 16865px — so the group scales
   during the transition. That reads as the page being inset from the edges
   with softened corners, snapping square when the animation ends. Holding the
   group at the incoming geometry removes the scale.

   THE HEADER HAS NO view-transition-name, deliberately, and that is the fix
   for the curved top-left corner on the announcement bar.

   Naming it promoted it to its own compositing layer. A separate layer gets
   the browser's rounded content-area corner applied to ITSELF, instead of the
   page being clipped invisibly behind it — so the maroon bar rendered with a
   curved corner and a sliver of gap beside it, on load and on resize. Naming
   it also created a second transition group whose geometry differs between
   pages (the splash has no header at all), which then had to be pinned
   separately to stop it scaling.

   Both problems vanish if the header simply travels with the page as part of
   the root snapshot. The cost is that it slides rather than staying fixed for
   the 620ms of a transition. That is the better trade, and it removes a whole
   class of compositing bugs rather than patching each one.

   Worth keeping: the symptom that identified this was the header MEASURING a
   correct 0..viewport while visibly painting inset and curved. An element that
   measures right and paints wrong is a compositing or snapshot problem, never
   a box-model one — checking border-radius, margin, max-width and transform
   found nothing, three times over, because none of those were involved. */
html[data-vt]::view-transition-group(root){
  animation:none;
}
html[data-vt]::view-transition-old(root){
  z-index:1;
  mix-blend-mode:normal;
  background-color:var(--gp-background-default);
}
html[data-vt]::view-transition-new(root){
  animation:none;
  opacity:1;
  mix-blend-mode:normal;
}

html[data-vt="forward"]::view-transition-old(root){
  animation:gpSlideOutLeft 620ms cubic-bezier(.55,.06,.24,.98) both;
}
html[data-vt="back"]::view-transition-old(root){
  animation:gpSlideOutRight 620ms cubic-bezier(.55,.06,.24,.98) both;
}

@media(prefers-reduced-motion:reduce){
  html[data-vt]::view-transition-old(root){animation:none;}
}

/* ------------------------------------------------------------ third polaroid
   Tarun, added alongside the two founders.

   It carries BOTH .jcpolaroid and .tarunpolaroid. The first is load-bearing:
   the Webflow interaction that fades and lifts the second polaroid targets the
   CSS selector ".jcpolaroid" rather than a specific element id, so any element
   with that class inherits the animation. An invented data-w-id would NOT
   work — the interaction payload is compiled and cannot be extended by markup.

   .tarunpolaroid then re-positions it, since it would otherwise land exactly
   on top of the polaroid it borrowed the animation from. Overrides are needed
   at every breakpoint where .jcpolaroid sets top/left: base, 1280, 991, 479. */
/* Stacking: Tarun sits BEHIND the two founders so its lower edge is overlapped
   by them. Both rules are single-class, so equal specificity — .tarunpolaroid
   must come second to win, since that element carries .jcpolaroid too. */
.jbpolaroid,.jcpolaroid{z-index:2;}

/* HOW THE PER-BREAKPOINT NUMBERS BELOW ARE ARRIVED AT — read this before
   changing any of them.

   Tarun is positioned in the same containing block as the Cartwright polaroid
   but with its own top/left, so the two are only related by whatever numbers
   happen to be written here. Tuning those numbers by eye, one breakpoint at a
   time, is what broke this twice: once leaving Tarun stranded above the
   cluster, then once burying it underneath.

   There is only one relationship, and the wide breakpoints already express it.
   Measured there — the ones that look right — Tarun sits UP AND LEFT of
   Cartwright by:

       dx = -0.53 x Cartwright width      dy = -0.30 x Cartwright height

   (1440px: -0.53 / -0.33.  1100px: -0.54 / -0.28.)

   Every other breakpoint's top/left is that same offset, computed against the
   Cartwright box at that width, NOT a fresh guess. To re-derive after any
   change to .jcpolaroid, measure both boxes at the width in question and solve
   for the top/left that puts Tarun at those fractions.

   Sign matters more than magnitude here: dx and dy must both stay NEGATIVE.
   Positive values put Tarun behind Cartwright instead of peeking out from
   behind its top-left corner, which is how it disappeared. */

/* drop-shadow, not box-shadow: the PNG is transparent around a tilted polaroid,
   and box-shadow would trace the rectangular element box instead of the art.
   The other two have their shadow baked into their PNGs; this matches it. */
.tarunpolaroid{
  max-width:274px;min-width:0;
  top:-250px;left:-74px;
  z-index:1;
  filter:drop-shadow(0 5px 9px rgba(0,0,0,.20));
  /* Because this element borrows the Cartwright polaroid's interaction by
     class, it receives an IDENTICAL transform on every scroll frame and the
     two move in perfect lockstep, which reads as one rigid object. The
     interaction payload is compiled and cannot be retimed, so instead the
     element eases TOWARDS each transform it is given. Same destination, a
     beat behind — it trails on the way in and settles a moment later.
     Transform only: opacity stays in step so both still fade together. */
  transition:transform 300ms cubic-bezier(.22,.61,.36,1);
}
@media screen and (min-width:1280px){
  .tarunpolaroid{width:280px;max-width:280px;top:-262px;left:-36px;}
}
@media screen and (max-width:991px){
  /* Below 992px the "Thirty years on" copy sits directly under the cluster and
     the desktop offset covered it — 115px of horizontal overlap measured at
     960px. Cleared by moving RIGHT of the copy rather than above it, which
     keeps the polaroid low in the cluster and still tucked behind Cartwright. */
  /* Right of the copy AND raised. Shifting right alone was not enough: the
     Cartwright polaroid then covered the caption, and pushing far enough right
     to clear it (left ~258px) detached this polaroid from the cluster and ran
     it toward the viewport edge. Raising it clears the caption while keeping
     the three reading as one group. */
  /* Started from the derived -0.53w / -0.30h offset (top -247), then down 100
     and back up 50 by eye — net -197, so dy is about -0.16h here against the
     -0.30 the other widths share. A deliberate departure at this width only,
     recorded so it is not mistaken for drift. Left is unchanged; the cluster
     moves as a whole via .polaroidwrapper. */
  .tarunpolaroid{max-width:252px;top:-197px;left:-100px;}
}
@media screen and (max-width:479px){
  /* Raised and pulled left so the Cartwright polaroid stops clipping the
     caption. At 375px the cluster is tight: shifting left alone cannot clear
     it (Cartwright's left edge lands mid-caption), so most of the correction
     is vertical. */
  /* Same derivation, against this breakpoint's Cartwright size of 211x239. */
  .tarunpolaroid{max-width:54%;top:-133px;left:-22px;}
}

/* ------------------------------------------- copy clearance beside Cartwright
   On wide viewports the "Thirty years on" block ran into the Cartwright
   polaroid: measured at 1400px, the block's left edge sat at 613 against the
   polaroid's right edge at 640 — 27px of actual overlap, not merely a tight
   gap. Held across the whole scroll range, so it is a layout fact rather than
   something the interaction was doing.

   The block is moved right rather than the polaroid left: the three polaroids
   are one composed cluster, and nudging one of them to solve a text problem
   would be fixing the wrong element. Margin, not transform — these blocks
   carry IX2 interactions, and a transform here would be overwritten the moment
   one runs.

   Only above 991px. Below that the copy sits under the cluster, not beside it,
   and there is nothing to clear. */
@media screen and (min-width:992px){
  .storytxtblock.right{margin-left:75px;}
}
/* Below 992px the copy is not beside the cluster, it is interleaved with it —
   the polaroids are absolutely positioned, so nothing in flow accounts for the
   space they occupy and the copy rides up through them. Measured at 780px: the
   cluster's lowest edge is 2011 and the copy's first inked line was at 1496,
   so the text ran straight through Cartwright and Bostock.

   Pushed below the cluster. The number is the measured clearance plus a 28px
   gap, not a guess — but it IS tied to the current polaroid sizes, so re-measure
   if any of them change: cluster bottom minus copy ink top, plus the gap. */
/* Only the phone stacks. Between 480 and 991 the copy belongs to the LEFT of
   the cluster, not under it — pushing it down there dropped it off the bottom
   of the section entirely. The cluster is shifted right instead, below, which
   is what makes room for it. */
@media screen and (max-width:479px){
  /* The three polaroids are sized in PERCENTAGES below 479px (100%/70%/54%),
     so their heights shrink with the viewport while a fixed margin here does
     not. Measured gap from the artwork's visible bottom edge to the first line
     of copy: 13px at 479 wide, 83px at 390, 137px at 320 — the images appear to
     fly away from the text as the phone narrows. Fitted across those three
     widths, then re-fitted once the polaroids' own scroll interaction was held
     at a fixed point in its travel, the gap moves 0.642px per px of viewport
     short of constant, so 64.2vw cancels it and the +78px sets what it settles
     at (~40px at 479, 430, 390 and 320 alike).
     Re-fit if the polaroid percentages or the copy's font size change. */
  .storytxtblock.right{margin-top:calc(64.2vw + 78px);}
}

/* --------------------------------------------------- italic optical alignment
   An earlier version nudged the opening italic right by 6.9px and was removed,
   on canvas TextMetrics showing actualBoundingBoxLeft of only -0.38px. That
   measurement was of the GLYPH, and it was the wrong thing to measure.

   Measured again 8 Sep against what the browser actually lays out, reading the
   span boxes rather than the font: in .storytxtblock.right every SUI span
   starts at x=90 and both GAR italic spans start at x=87, with margin-left 0
   on all of them. A consistent 3px, from the Garamond font's own metrics, and
   it shows whenever an italic span happens to begin a line — which the copy
   change on 8 Sep made happen twice.

   3px of padding puts the italic box on the same edge as the roman. Where an
   italic falls mid-line instead it gains 3px of space before it, which reads as
   ordinary word spacing at this size and does the italic no harm.

   The capital T keeps about 3px of ink beyond its box, which is the letterform
   overhanging and is left alone deliberately — that is optical alignment, not a
   defect. Do not chase it with a per-phrase nudge; the copy will change and the
   nudge will be attached to the wrong words. */
.storytxtblock .garamond-italic{padding-left:3px;}

/* --------------------------------------------------------------- line spacing
   One leading for these two blocks at every width, tight enough to hold the
   copy together.

   The uneven gaps on mobile were not a mobile bug. The spans carry DIFFERENT
   type sizes and their own line-heights — Garamond italic 31.2px/31.2, Suisse
   25.6px/19.2, founders 16px/16 — and the block's own line-height was 16px,
   below all of them. A line box grows to its tallest inline box, so whichever
   font happened to land on a line decided that line's height: the line holding
   "Thirty years on" got the 31.2px Garamond box and every other line got less.
   Desktop only looked even because its 37.5px cleared them all, and that is
   also why it read loose.

   34px is set on the block AND on every span inside it, so no inline box can
   grow past it and each line is the same height whatever font is on it. It sits
   just above the 31.2px Garamond, which is the tallest thing here — the reason
   for the value, and what to re-check if a type size changes.

   One value at all widths rather than per-breakpoint: the span sizes are
   already identical across breakpoints, so there was nothing for the two
   different values to be responding to.

   Every descendant, not just span: the copy nests <strong> and <em> inside the
   spans and those carry their own line-heights too. Setting it on the spans
   alone left the advance at 37.5px, because a <strong> inside was still taller
   than the line it sat on. */
.storytxtblock.right,.storytxtblock.right *,
.storytxtblock.neighbourhood,.storytxtblock.neighbourhood *{line-height:34px;}
/*

   The ragged left edge visible at desktop is not a defect either:
   .storytxtblock.right is text-align:right in gp-ecom.webflow.css, so lines
   legitimately begin at different x. Changing that is a design decision, not
   an alignment fix, and belongs in a deliberate change to text-align. */

/* All three polaroids nudged left together. Applied to the WRAPPER, not to the
   three images: .jbpolaroid is in flow and the other two are absolutely
   positioned against this box, so moving the box moves all three in lockstep
   by construction and cannot drift out of register.

   margin-left, not left: gp-ecom.webflow.css sets left on this wrapper at
   TWO widths — 143px from 1440px up and 159px from 1920px up. Overriding left
   would flatten that progression to a single value; a negative margin is
   additive and preserves it at both. */
/* Scoped to 1440px and up, NOT 1280px. gp-ecom.webflow.css only offsets this
   wrapper from 1440px (left:143px) and 1920px (left:159px); between 1280 and
   1439 it sits unoffset and the cluster is already close to the "Thirty years
   on" copy. Shifting there pushed the Cartwright polaroid 27px over that
   paragraph's box, which would have covered the start of the first line once
   it faded in. */
@media screen and (min-width:1440px){
  .polaroidwrapper{margin-left:-20px;}
}

/* All three polaroids shifted right together at mobile. On the wrapper, so they
   move in lockstep; margin-left is additive to the Webflow rule, which sets
   left:auto here rather than a pixel value. */
/* The three polaroids move together — the wrapper carries all of them, so this
   is one shift, not three.

   158px, not the 100px asked for, and the reason is arithmetic rather than
   taste. At 900px the cluster is 499 wide and the copy's text 334; with a 24px
   gap that needs 857 of the 900 available, so the two only fit if BOTH move.
   At 100px they still overlapped by 94px. The copy is pulled 50px left below
   and the cluster takes the remaining 58 — 158 total. Gap lands at 14px with
   13px left at the right edge, which is as tight as this width allows.
   Anything wider than the copy, or any growth in the polaroids, and this
   breakpoint has to stack instead. */
@media screen and (max-width:991px) and (min-width:480px){
  /* margin-top pulls the wrapper and everything after it up together, which is
     what keeps the three polaroids rigid — shifting them individually would
     just be the same hand-tuning that broke this before. */
  .polaroidwrapper{margin-left:158px;margin-top:-100px;}
  .storytxtblock.right{margin-left:-50px;}
  /* The whole group down 40px so it sits centred in the section. Applied to
     .story-grid.right, which is the lowest common ancestor of the copy and the
     polaroid wrapper — one number that moves both, rather than 40px added to
     each and left to drift apart. */
  .story-grid.right{margin-top:40px;}
}
@media screen and (max-width:479px){
  /* Centred on the ARTWORK, not on the boxes. Each polaroid PNG carries a
     different transparent margin around its tilted photo (measured opaque
     bounds: Bostock 7.5-78.2% of its width, Cartwright 7.6-88.3%, Tarun
     1.9-95.8%), so the union of the three element boxes is centred while the
     visible cluster sits left of centre. Measured shift needed to centre the
     visible cluster: 30px at 479 wide, 17px at 390, 7px at 320 — linear in
     viewport width, hence the vw term rather than a fixed nudge. Fitted in two
     passes; the second corrects for the polaroids' scroll interaction, which
     translates them by a fraction of their own (viewport-proportional) size. */
  .polaroidwrapper{margin-left:calc(19.5vw - 43.6px);}
  /* Drops the copy AND the polaroids together — .story-grid.right is the
     offsetParent of .polaroidwrapper and also contains .storytxtblock.right,
     so one value moves the whole group and the two cannot drift apart.
     Measured 1:1 (60px -> 210px moved both by exactly 150px). At 210 and a
     932px-tall viewport the artwork spans 263-668 in the frame where the copy
     finishes revealing, i.e. a midpoint of 465.5 against a viewport centre of
     466 — the cluster is vertically centred and the copy sits ~65px off the
     bottom, at 479, 430 and 320 alike. */
  .story-grid.right{margin-top:210px;}
}

/* ------------------------------------------------- narrow-viewport overflow
   .storytxtblock carries min-width:400px in gp-ecom.webflow.css. At a 480px
   viewport that plus its offset pushed the About page 8px wider than the
   window, which shows as a horizontal scrollbar. Webflow's own <=479px rules
   already relax it, so this only covers the gap just above that breakpoint. */
@media screen and (max-width:560px){
  .storytxtblock{min-width:0;max-width:calc(100vw - 48px);}
}

/* ------------------------------------------------ Webflow transition curtain
   Off. It played a full-screen overlay on arrival at About and Jobs, reading
   as a stall before the page appeared. The markup stays — .transition and its
   children carry data-w-id attributes that scripts/verify.sh counts — so it is
   hidden here instead of deleted. See partials/footer.html for the full
   reasoning and how to restore it. */
.transition{display:none !important;}

/* --------------------------------------------------- view-transition backdrop
   html had no background of its own: body carries the cream and it propagates
   to the canvas. During a cross-document view transition the root snapshot can
   miss that propagated background, so the browser's white base showed through
   for a frame — the white flash between pages.

   Set on html explicitly. Uses --vellum, the same token body uses, NOT
   --gp-cream: those two are #fff8ee and #fff7ef, one to two values apart, and
   using the other one here would leave a faint seam instead of a flash. */
/* 🔴 color-scheme is what stops the flash between pages in a dark-mode browser.
   Without it the browser treats the page as scheme-agnostic and paints its OWN
   base canvas — dark, in dark mode — in the moment between one document going
   and the next painting. This site is cream on every page, so that moment
   reads as a dark blink on every navigation. Locally the swap is too fast to
   see; over a network it is 100-500ms and plainly visible.
   The design is light-only by intent, so declaring it is honest as well as a
   fix. Do not remove without checking in a browser set to dark mode. */
html{color-scheme:light;background-color:var(--gp-background-default);}

/* -------------------------------------------- narrow-viewport cluster overflow
   NOT FIXED — deliberately, and documented rather than half-fixed.

   Between 480px and 767px the About polaroid cluster overflows the viewport
   and the page scrolls sideways. Measured at 480px: the document scrolls 133px
   horizontally, with .polaroidwrapper reaching right=613 against a 480 viewport
   (82px at 560, 32px at 640, and 0 at both 375 and 768).

   It is the inherited Webflow layout, not the third polaroid: at these widths
   the wrapper sits at left:26.7vw and the founders' images are ~437px wide, so
   the row cannot fit. Tarun's polaroid reaches right=522, well inside the
   widest of them.

   Two attempts were made and BOTH failed, so neither is left in the file:
   overflow-x:clip on body computed as clip but html stayed visible, so it never
   reached the viewport; moving it to html made html compute clip and the page
   still scrolled 133px. Going further means overflow:hidden on the root, which
   would make html the scroll container — and the About polaroids animate on
   scroll, so that risks the thing this site exists to show. Not worth it the
   night before a demo for a scrollbar in one band on one page.

   Tracked as backlog item 13. The correct fix is to lay the cluster out so it
   fits, not to clip the root.

   One measurement lesson worth keeping: three earlier sweeps called this band
   clean. They sampled 250-400ms after load, and the polaroid <img> tags carry
   no width or height attributes, so the images still had zero width. Sample
   after they load, and test whether the page can actually SCROLL rather than
   reading scrollWidth, which reports content extent either way. */


/* Apply-form confirmation (About) ------------------------------------------
   The note sits inside .joinusform so it inherits the hand-placed offsets that
   put the form inside joinus-macwindow.svg. .joinusform is a column flexbox
   with a fixed height, so auto vertical margins centre the note in the panel
   the fields just vacated. Scoped to the join-us section: the Contact form's
   notes are siblings of the form and take the page's normal flow. */
.section-joinus .joinusform .gp-form__note{
  margin:auto 0;padding:0;text-align:center;
}

/* ── Decoration must not eat clicks ──────────────────────────────────────────
   Added 10 Sep 2026 after scripts/harness/overlap.js found three purely
   decorative elements sitting on top of links and taking their clicks.

   The stacking trap, which is worth understanding because it will recur:
   `.deluxetext` is `position: relative` and `.deluxe-container` is static.
   A POSITIONED element paints above a static one regardless of DOM order, so
   the scrolling "Deluxe Deluxe Deluxe" marquee — which appears EARLIER in the
   markup than the buttons — was still painted over them, and was taking the
   click on About's DIRECTIONS and DELIVERY buttons at eight of nine widths.
   Nobody reported it: a marquee is invisible in a screenshot and the buttons
   look perfectly normal.

   `pointer-events: none` is the whole fix. It changes nothing visual, touches
   no layout and cannot reorder anything — it simply says these three are
   scenery. Raising a z-index instead would have meant reasoning about paint
   order in a hand-tuned Webflow collage, which is how this got broken.

   These overrides live here rather than in gp-ecom.webflow.css because that
   file is frozen (docs/DELIBERATE.md) and site.css is last in the cascade. */
.deluxetext,      /* the scrolling "Deluxe" marquee */
.whiteglobe,      /* the globe drawn on the APPLY TODAY button */
.googlelogo,      /* the Google mark on the DIRECTIONS button */
.deliveroologo,   /* the Deliveroo mark on the DELIVERY button */
.beigebutton {    /* the pill graphics themselves — see below */
  pointer-events: none;
}
/* .beigebutton is the button's own artwork, and it needed this too. The markup
   is <div.deluxe-button> → two <img.beigebutton> → <img.googlelogo> →
   <a.buttonoverlay>. The images come FIRST, and each carries a transform
   (rotate(-.2deg)), which gives it a stacking context and lifts it out of the
   static layer — so the pill was painting over the very link that sits inside
   the same button. Every one of these five is decoration; the anchor is the
   only thing that should ever answer a click, and it spans the whole pill at
   every width tested. */

/* ── The wage panel keeps its box ────────────────────────────────────────────
   Added 10 Sep 2026, immediately after replacing the panel copy.

   JC's new wording — "We pay competitively!" — is one line where the old
   Living Wage sentence was five. `.livingwage` is `position: relative`, so it
   still occupies its natural height in flow: the text shrank from ~105px to
   ~15px and everything after it rose by ninety-odd pixels. `overlap.js` caught
   the consequence straight away — "Apply Today!" landed on the first form
   field at 480, 767 and 991, on both About and Jobs. It was not there before
   the copy change; the gate ran before and after and the diff is unambiguous.

   The panel is a fixed slot in a drawn mac-window graphic, not a box that
   should resize with its contents. Holding its previous height puts every
   element below back exactly where the design put it, and the new copy simply
   sits higher in the same panel. Values are the heights measured before the
   change, per breakpoint. */
.livingwage {
  min-height: 105px;
  /* Bigger and centred, 10 Sep 2026 at James's request. The Webflow original
     was .7rem and text-align:justify — justification set for a five-line
     paragraph, which turns three short lines into three stretched ones.
     --gp-size-label rather than a raw px value: site.css is an AUTHORED file
     and check-design.sh's scale ratchet is at its budget of 5. */
  font-size: var(--gp-size-label);
  text-align: center;
  line-height: 1.45;
}
@media screen and (max-width: 991px) { .livingwage { min-height: 77px; } }
@media screen and (max-width: 479px) { .livingwage { min-height: 90px; } }

/* ── "Corner Shop" fits its window ───────────────────────────────────────────
   Backlog 28's last survivor, fixed 10 Sep 2026. Found by clipping.js: at
   480px the headline was cut mid-word with only 79% of it visible.

   The cause is a seam between two rules. Below 480 the type is `14vw` — fluid,
   so it always fits. From 480 to 991 it is a fixed `90px`, which renders 541px
   wide. At a 480px window that is 112px off the right edge; by 600px it fits
   again. So it is broken in a band roughly 480–592 and correct on both sides
   of it, which is exactly the signature of a missing breakpoint rather than a
   deliberate bleed. (The slanted BAND behind it is 120vw and is meant to run
   off the page. The words are not.)

   min() removes the seam instead of adding another one: fluid while 14vw is
   the smaller number, pinned at 90px from ~643px up, and no jump at the
   crossover. */
@media screen and (max-width: 991px) {
  .cornershop.cs, .cornershop.deluxe { font-size: min(90px, 14vw); }
}

/* ── The DIRECTIONS / DELIVERY labels ride with their anchor ─────────────────
   Fixed 10 Sep 2026, and it is a regression I introduced earlier the same day.

   Backlog 40's sibling fix moved `.buttonoverlay` from left:-490px to -329px at
   ≤479, so the clickable anchor finally covered the button it belongs to. But
   `em.buttontext` is a CHILD of that anchor and is positioned relative to it,
   so the visible label travelled the same 161px and ended up beside the button
   rather than on it — sitting over the opening hours. James spotted it on an
   iPhone.

   The anchor is invisible; the label is the thing people read. Both need to be
   on the button, so the label gets the same 161px back:
     directions  191px → 30px      delivery  192px → 31px
   Measured, not guessed: the anchor moved +161, so the child moves −161. */
@media screen and (max-width: 479px) {
  .buttontext.directions { left: 30px; }
  .buttontext.delivery   { left: 31px; }
}

/* ── Why the founders' block is NOT shifted up on a phone ───────────────────
   James asked for the polaroids and the "Thirty years on" copy to move up
   100px at the phone breakpoint. It was done — `.story-grid.right { top:
   -616px }` — and then reverted the same hour, because he spotted that the
   Tarun polaroid looked higher relative to the others.

   **Tarun did not move. The Bostock polaroid lagged.** Measured before and
   after at identical viewport positions: Tarun and Cartwright were
   pixel-identical with the same fixed `translateY(-101.25)`. `.jbpolaroid`
   carries a SCROLL-DRIVEN IX2 transform whose value is fractional and changes
   as you scroll — it travels about 157px relative to the other two.

   Shifting the block moves WHEN that animation runs, not where it ends:

     wrapper at   original   with the 100px shift
        500px       -19            -14
        300px       -44            -14
        200px      -110            -14
        100px      -171            -55

   At the moment the cluster is on screen, Bostock had not caught up, so the
   composition reads wrong. The timing lives in the vendored IX2 runtime, which
   this repo does not edit, so there is no way to re-sync it from CSS.

   🔴 **Do not move this block vertically at any breakpoint.** If the space
   above it needs closing, change what sits ABOVE it — the animation is bound
   to this block's own scroll position, so anything that moves it will
   desynchronise the same way. This is exactly the class of defect CLAUDE.md
   warns the visual harness cannot see: "it cannot see interaction state,
   animation timing". */
