/* ==========================================================================
   GRO - service page components
   Loaded after styles.css on every service page. Nothing here overrides a
   token; everything is built from the ones styles.css already defines, so a
   change at the token layer still reaches this file.

   The four service pages share this stylesheet. If a rule only ever applies
   to one of them it does not belong here - put it in that page's own block
   at the foot of the file and say which page it is for.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Hero
   Nav is fixed, so the top padding carries its height on top of the section
   rhythm. The headline and the lead are measured separately: 16ch breaks the
   headline into three deliberate lines, while the lead keeps a normal reading
   measure underneath it.
   ========================================================================== */
.svc-hero{
  padding-block:calc(var(--section-y) + 64px) var(--section-y);
  background:var(--surface-page);
}
/* 7.5rem/120px, up from the 5.75rem the type scale gives .display-1. Measured
   against the 1136px container: the longest of these headings ("A site that
   loads before they lose interest.", 18.9x the font size) still lands on two
   lines at 120px and tips to three at 130, so every service hero is a two-line
   heading at about three quarters of the measure.

   max-width is off rather than 16ch. At 120px, 16ch computes to roughly 1244px
   and stops constraining anything anyway, so the cap was doing nothing but
   hiding which limit was actually in play - it is the container.

   text-wrap:balance stays: these are single sentences with no deliberate break
   in them, so evening the two lines is what we want. The SEO hero opts out
   below, because its break is chosen rather than found. */
.svc-hero__copy .display-1{
  margin-top:var(--space-4);
  font-size:clamp(3rem,8.6vw,7.5rem);
  max-width:none;
  text-wrap:balance;
}
.svc-hero__copy .lead{max-width:60ch;margin-top:var(--space-6)}
.svc-hero__actions{display:flex;flex-wrap:wrap;gap:var(--space-4);margin-top:var(--space-10)}

@media (max-width:640px){
  .svc-hero{padding-block:calc(var(--section-y) + 40px) var(--section-y)}
  .svc-hero__actions .btn{width:100%}
}

/* --------------------------------------------------------------------------
   The race - this page's signature
   A filmstrip of two builds at 0.4 / 0.8 / 1.2 / 2.4 and 4.0 seconds. The
   playhead runs on a real clock (service.js), so the gap between the two
   tracks is not illustrated, it is waited through.

   The frames are wireframe motifs, not screenshots: the guidelines rule out
   stock and AI imagery and ask for a pine panel with a live data element
   where imagery is wanted, which is exactly what this is.
   ========================================================================== */
.race{
  margin:clamp(48px,7vw,88px) 0 0;padding:clamp(20px,3vw,32px);
  background:var(--surface-inverse);border:1px solid var(--border-inverse);
  border-radius:var(--radius-2xl);
}

.race__head{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:var(--space-6);flex-wrap:wrap;
}
.race__note{
  margin-top:var(--space-1);font-size:var(--size-xs);
  color:var(--text-on-dark-muted);max-width:52ch;
}
.race__replay{flex:none}

/* Elapsed clock. The rail is the full four seconds; --p is written by the
   script as a 0-1 progress value. */
.race__clock{
  display:flex;align-items:center;gap:var(--space-4);
  margin-top:var(--space-6);
}
.race__rail{
  position:relative;flex:1;height:2px;border-radius:2px;
  background:rgba(234,242,237,.14);
}
.race__playhead{
  position:absolute;inset-block:0;left:0;width:calc(var(--p,0) * 100%);
  background:var(--jade-400);border-radius:2px;
}
/* The head of the playhead, so the position reads at a glance. */
.race__playhead::after{
  content:"";position:absolute;right:-4px;top:-3px;
  width:8px;height:8px;border-radius:50%;background:var(--jade-400);
}
.race__readout{
  font-family:var(--font-display);font-weight:700;font-size:var(--size-sm);
  color:var(--jade-400);font-variant-numeric:tabular-nums;
  min-width:4ch;text-align:right;
}

.race__track{margin-top:var(--space-8)}
.race__label{
  display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-4);
  flex-wrap:wrap;
  font-family:var(--font-display);font-weight:700;font-size:var(--size-h4);
  letter-spacing:var(--tracking-tight);color:var(--text-heading);
  margin-bottom:var(--space-4);
}
.race__label span{
  font-family:var(--font-body);font-weight:500;font-size:var(--size-xs);
  letter-spacing:0;color:var(--text-on-dark-muted);
}

.race__frames{
  display:grid;grid-template-columns:repeat(5,1fr);gap:var(--space-3);
  list-style:none;margin:0;padding:0;
}

/* A frame is dark and dashed until the playhead reaches its timestamp, then
   it settles in. Opacity and transform only - nothing here reflows. */
.race__frame{
  position:relative;padding:10px;border-radius:var(--radius-sm);
  background:var(--pine-900);border:1px dashed rgba(234,242,237,.16);
  opacity:.34;transform:translateY(4px);
  transition:opacity var(--dur-base) var(--ease-out),
             transform var(--dur-base) var(--ease-out),
             border-color var(--dur-base) var(--ease-out);
}
.race__frame.is-lit{opacity:1;transform:none;border-style:solid}

.race__t{
  display:block;margin-top:8px;
  font-family:var(--font-display);font-weight:700;font-size:var(--size-xs);
  color:var(--text-muted);font-variant-numeric:tabular-nums;
}
.race__frame.is-lit .race__t{color:var(--text-body)}

/* The painted content of a frame. Three states, and the state is the whole
   argument: nothing, a header, or a usable page. */
.race__paint{
  display:flex;flex-direction:column;gap:5px;justify-content:flex-start;
  aspect-ratio:4 / 3;padding:7px;border-radius:6px;
  background:rgba(6,31,23,.85);
}
.race__paint i{display:block;height:5px;border-radius:2px;background:var(--mist-300)}
.race__paint i:first-child{background:var(--jade-400);height:7px}
.race__paint--part i:last-child{width:56%}
.race__paint--full i:nth-child(2){width:88%}
.race__paint--full i:nth-child(3){width:72%}
.race__paint--full i:last-child{width:44%;background:var(--jade-400);margin-top:auto}

/* Empty is not styled as an absence - it is labelled as one. Ember is the
   guidelines' warm counterpoint, used here and nowhere else on the page, to
   mark the seconds the visitor spends looking at nothing. */
.race__paint--none{position:relative}
.race__paint--none::after{
  content:"blank";position:absolute;inset:0;display:grid;place-items:center;
  font-family:var(--font-body);font-weight:700;font-size:9px;
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;
  color:var(--ember-400);opacity:.72;
}

/* The slow track's lit frames still read as the losing side. */
.race__track--slow .race__frame.is-lit{border-color:rgba(245,167,66,.28)}
.race__track--gro .race__frame.is-lit{border-color:var(--border-inverse)}

@media (max-width:720px){
  /* Five frames across a phone leaves each one 50px wide and unreadable.
     Three columns keeps the shape of the argument - two rows per track. */
  .race__frames{grid-template-columns:repeat(3,1fr)}
  .race__label{font-size:var(--size-body)}
}

@media (prefers-reduced-motion:reduce){
  /* Nothing sweeps, so nothing is withheld: every frame is present and the
     timestamps carry the argument on their own. */
  .race__frame{opacity:1;transform:none;border-style:solid;transition:none}
}

/* --------------------------------------------------------------------------
   Section intro - the shared heading block every service section opens with
   ========================================================================== */
.svc-intro{max-width:640px}
.svc-intro .display-2{margin-top:var(--space-4);text-wrap:balance}
.svc-intro .lead{margin-top:var(--space-6)}

/* --------------------------------------------------------------------------
   Banner headlines - work.html
   The same device the landing page uses on .growth__headline and
   .partner__title: a headline run across the whole container so it reads as a
   banner over its section rather than as the top of a column of copy.

   Three things have to be undone for that to work, and all three are why a
   plain font-size bump would not have done it:

   1. .svc-intro caps everything at 640px, which is what was making these wrap
      early into two short lines. The cap comes off the block - but the lead
      keeps a measure of its own, because 1136px of body copy is unreadable.
   2. text-wrap:balance comes off. Balance evens lines out by making them
      shorter, which is precisely the opposite of filling the width.
   3. The size is tuned to the wording, not taken from the type scale. Each cap
      below is the measured size at which that exact sentence lands on one line
      just inside the 1136px container, with a little under a percent in hand.

   SO: CHANGING EITHER HEADLINE'S WORDING BREAKS ITS FIT. Re-measure rather
   than assuming the number still holds - a longer sentence will wrap to two
   lines at these sizes, a shorter one will stop short of the margin.

   The vw term is set so the cap starts binding at a 1200px viewport, which is
   where the container reaches its own maximum; below that both the text and
   the container scale together, and the 2.25rem floor catches the phone end.
   That floor is the same one both landing page banners use.
   ========================================================================== */
.svc-intro--banner{max-width:none}
.svc-intro--banner .display-2{
  text-wrap:normal;                  /* see (2) above */
  line-height:var(--leading-display);
  letter-spacing:var(--tracking-display);
}
.svc-intro--banner .lead{max-width:60ch}

/* Measured at 1136px of container: "Sites we built, still doing the work."
   lands on one line at 4.63rem, so the cap sits just under it. */
#partners .svc-intro--banner .display-2{font-size:clamp(2.25rem,6vw,4.5rem)}

/* Measured the same way: "The data tells a success story." lands on one line
   at 5.21rem. It is the shorter sentence, so it takes the larger size - which
   is why these two numbers are not the same and should not be merged. */
#data .svc-intro--banner .display-2{font-size:clamp(2.25rem,6.8vw,5.1rem)}

/* And again: "Kind words from our longest partner." lands on one line at
   4.29rem. Nearly the same length as the partners headline, so nearly the same
   number - but measured, not copied from it. */
#words .svc-intro--banner .display-2{font-size:clamp(2.25rem,5.6vw,4.2rem)}

/* --------------------------------------------------------------------------
   What ships
   A stack of service names set at display size, idle in mist and lit as you
   move down them, with the detail folded underneath each one.

   It replaced a two-column definition list. The list showed everything at
   once and read as a specification document - accurate, and completely flat:
   twelve equal rows with nothing to catch the eye and no reason to start at
   the top. Setting the names at display size makes the section scannable in
   two seconds, and folding the detail away means a visitor chooses which
   commitments to read rather than being handed all twelve at once.

   Built on <details>/<summary>, so it opens without JavaScript, takes keyboard
   focus and arrow keys for free, and is announced as expandable - the same
   reason .faq is built that way. The hover lift is decoration on top of a
   control that already works.

   ONE ITEM IS OPEN ON LOAD (the first). Collapsing all twelve would leave a
   visitor with a bare list of nouns and no evidence the page has anything
   behind it.
   ========================================================================== */
.ships{
  margin:clamp(40px,5vw,64px) 0 0;
  border-top:1px solid var(--border-inverse);
}
.ships__item{border-bottom:1px solid var(--border-inverse)}

/* The negative inline margin lets the hover band bleed past the text column
   so it reads as a full-width row rather than a box drawn around the words. */
.ships__row{
  display:grid;grid-template-columns:48px 1fr auto;
  align-items:center;gap:var(--space-5);
  padding:var(--space-6) var(--space-5);
  margin-inline:calc(var(--space-5) * -1);
  border-radius:var(--radius-md);
  cursor:pointer;list-style:none;
  transition:background var(--dur-fast) var(--ease-out);
}
.ships__row::-webkit-details-marker{display:none}
.ships__row::marker{content:""}
.ships__row:focus-visible{outline:none;box-shadow:var(--focus-ring)}

.ships__n{
  font-family:var(--font-body);font-weight:700;font-size:var(--size-eyebrow);
  letter-spacing:var(--tracking-eyebrow);color:var(--jade-400);
  font-variant-numeric:tabular-nums;align-self:center;
}
/* Idle is mist-300, not mist-400. mist-400 dims harder and looks better, but
   it measured 3.24:1 on the pine panels - a pass only because these names are
   large text, and one clamp tweak away from failing. These are names a visitor
   reads to scan the section, not decoration. mist-300 gives 5.52:1 on pine and
   8.66:1 on night, clearing even the stricter normal-text bar at any size.

   The dim does not have to carry the active state on its own anyway: the lit
   row also gets a jade band behind it and a plus that rotates to a cross. */
.ships__name{
  font-family:var(--font-display);font-weight:700;
  font-size:clamp(1.75rem,3.4vw,2.75rem);
  line-height:var(--leading-tight);letter-spacing:var(--tracking-display);
  color:var(--mist-300);
  transition:color var(--dur-base) var(--ease-out);
  text-wrap:balance;
}
/* Plus, rotating to a cross when the item is open. */
.ships__go{
  flex:none;display:grid;place-items:center;
  width:38px;height:38px;border-radius:50%;
  border:1px solid var(--border-default);color:var(--jade-400);
  transition:transform var(--dur-base) var(--ease-out),
             border-color var(--dur-fast) var(--ease-out),
             background var(--dur-fast) var(--ease-out);
}

/* Lit: hovered, focused, or open. Kept as one rule so the three states are
   identical - a keyboard user gets exactly what a mouse user gets. */
.ships__row:hover,
.ships__row:focus-visible,
.ships__item[open] > .ships__row{background:rgba(74,222,128,.07)}
.ships__row:hover .ships__name,
.ships__row:focus-visible .ships__name,
.ships__item[open] > .ships__row .ships__name{color:var(--mist-100)}
.ships__row:hover .ships__go,
.ships__row:focus-visible .ships__go{border-color:var(--jade-400);background:rgba(74,222,128,.10)}
.ships__item[open] > .ships__row .ships__go{
  transform:rotate(45deg);border-color:var(--jade-400);background:rgba(74,222,128,.10);
}

/* Indented to start under the name rather than under the number. */
.ships__body{
  padding:0 var(--space-5) var(--space-8) calc(48px + var(--space-5));
  max-width:72ch;
}
.ships__body p{
  color:var(--mist-200);line-height:var(--leading-body);font-size:var(--size-lead);
}

@media (max-width:720px){
  .ships__row{grid-template-columns:34px 1fr auto;gap:var(--space-4);padding-inline:var(--space-4)}
  .ships__row{margin-inline:calc(var(--space-4) * -1)}
  .ships__go{width:32px;height:32px}
  .ships__body{padding-left:calc(34px + var(--space-4));padding-right:0}
  .ships__body p{font-size:var(--size-body)}
}

@media (prefers-reduced-motion:reduce){
  .ships__row,.ships__name,.ships__go{transition:none}
}

.svc-aside{margin-top:var(--space-8)}


/* --------------------------------------------------------------------------
   Objections
   The .faq rules moved to styles.css when the grant section on index.html
   needed them - index.html does not load this file. Nothing else changed;
   every page using .faq loads styles.css, so the markup is untouched.
   ========================================================================== */

/* --------------------------------------------------------------------------
   Closing band
   Sets the whole band: ground, measure, alignment and rhythm. The .cta rules
   this used to lean on were for the jade version and have been deleted.
   ========================================================================== */
/* Dark and centred, not a jade band. The jade version put the loudest colour
   on the site immediately above a pine footer and left the button, the one
   thing that should be loudest, as the quietest element in it - pine on jade.
   Inverting it gives the button the brand colour back.

   The section no longer carries .cta or .section--jade: .cta hard-sets the
   heading and paragraph to pine for a jade ground, which is wrong here. Those
   rules are now unused by anything (see the note in styles.css).

   The glow is two soft jade radials rather than a flat panel, which is what
   separates this band from the footer directly beneath it - both are near
   black, and a flat ground would let the two run together. */
.svc-cta{
  position:relative;overflow:hidden;
  background:var(--surface-page);
  text-align:center;
  border-top:1px solid var(--border-subtle);
}
.svc-cta::before{
  content:;position:absolute;inset:0;pointer-events:none;
  background:
    radial-gradient(54% 72% at 82% 6%, rgba(74,222,128,.17), transparent 62%),
    radial-gradient(44% 62% at 10% 98%, rgba(74,222,128,.08), transparent 64%);
}
/* Wider than --container-narrow (760px), which forced the heading to three
   lines at banner scale. 920px lands it on two, and the lead keeps its own
   46ch measure regardless, so only the heading gains the room. */
.svc-cta .container{position:relative;z-index:1;max-width:920px}
.svc-cta .svc-headline{margin-top:var(--space-4);text-wrap:balance}
.svc-cta .lead{margin:var(--space-6) auto 0;max-width:46ch}
.svc-cta .btn{margin-top:var(--space-10)}

@media (max-width:560px){
  .svc-cta .btn{width:100%}
}

/* --------------------------------------------------------------------------
   Hero, short
   The contact page has no signature element under the copy - the form is the
   thing - so the hero does not need to leave room for one.
   ========================================================================== */
.svc-hero--short{padding-block:calc(var(--section-y) + 56px) calc(var(--section-y) * .6)}

/* --------------------------------------------------------------------------
   Signature element - shared shell
   Held as a shared shell because the service pages each carried an interactive
   figure under the hero. The shortlist and the pack have both since come off
   seo.html, so marketing's ledger is the last one using it - kept grouped so a
   new figure inherits the same box rather than inventing another.
   ========================================================================== */
.ledger{
  margin:clamp(48px,6vw,80px) 0 0;
  padding:clamp(20px,3vw,32px);
  background:var(--surface-card);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-xl);
  box-shadow:var(--shadow-md);
}
.ledger__head{
  display:flex;align-items:flex-start;justify-content:space-between;
  gap:var(--space-6);flex-wrap:wrap;
}
.ledger__note{
  margin-top:var(--space-3);max-width:62ch;
  font-size:var(--size-sm);line-height:var(--leading-snug);color:var(--text-body);
}

/* --------------------------------------------------------------------------
   The ledger - marketing.html
   Arithmetic on the visitor's own numbers rather than a statistic quoted at
   them. Defaults are written into the markup, so the static state is correct
   and coherent with the script switched off.
   ========================================================================== */
.ledger__reset{flex:none}
.ledger__controls{
  display:grid;grid-template-columns:repeat(3,1fr);
  gap:var(--space-8);margin-top:var(--space-8);
}
.ledger__ctrl{display:grid;gap:var(--space-3)}
.ledger__ctrl label{
  font-family:var(--font-body);font-weight:700;font-size:var(--size-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--mist-300);
}
.ledger__ctrl input[type="range"]{
  -webkit-appearance:none;appearance:none;
  width:100%;height:6px;margin:0;cursor:pointer;
  background:var(--night-600);border-radius:var(--radius-pill);
}
.ledger__ctrl input[type="range"]:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.ledger__ctrl input[type="range"]::-webkit-slider-thumb{
  -webkit-appearance:none;appearance:none;
  width:20px;height:20px;border-radius:50%;cursor:pointer;
  background:var(--jade-400);border:3px solid var(--surface-card);
}
.ledger__ctrl input[type="range"]::-moz-range-thumb{
  width:20px;height:20px;border-radius:50%;cursor:pointer;
  background:var(--jade-400);border:3px solid var(--surface-card);
}
.ledger__val{
  font-family:var(--font-display);font-weight:700;font-size:var(--size-h3);
  letter-spacing:var(--tracking-tight);color:var(--jade-400);
  font-variant-numeric:tabular-nums;
}

.ledger__out{
  display:grid;grid-template-columns:repeat(3,1fr);gap:1px;
  margin-top:var(--space-8);background:var(--border-subtle);
  border:1px solid var(--border-subtle);border-radius:var(--radius-lg);overflow:hidden;
}
.ledger__stat{
  background:var(--surface-sunken);padding:var(--space-6);
  display:flex;flex-direction:column;gap:var(--space-2);
}
/* The third figure is the argument, so it gets the raised panel and the jade. */
.ledger__stat--lead{background:var(--surface-raised)}
.ledger__label{
  font-family:var(--font-body);font-weight:700;font-size:var(--size-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--mist-300);
}
.ledger__num{
  font-family:var(--font-display);font-weight:700;
  font-size:clamp(1.5rem,3vw,2.25rem);letter-spacing:var(--tracking-tight);
  color:var(--text-heading);font-variant-numeric:tabular-nums;
}
.ledger__stat--lead .ledger__num{color:var(--jade-400)}
.ledger__sub{
  margin-top:auto;font-size:var(--size-sm);
  line-height:var(--leading-snug);color:var(--text-body);
}
.ledger__foot{
  margin-top:var(--space-6);max-width:72ch;
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--mist-200);
}

@media (max-width:860px){
  .ledger__controls{grid-template-columns:1fr;gap:var(--space-6)}
  .ledger__out{grid-template-columns:1fr}
}


/* --------------------------------------------------------------------------
   The kit - branding.html
   Six surfaces, one brand. The toggle is the argument rather than decoration:
   switching the guidelines off swaps colour, typeface, weight, tracking and
   corner radius on every tile at once, which is what actually happens to a
   business that never wrote any of it down.

   Each tile carries its own "loose" values inline, so the scrambled state is
   authored and reviewable rather than random - and so the same tile always
   falls apart the same way.
   ========================================================================== */
.kit__toggle{flex:none}

/* The switch inside the toggle. It reads the state off the button's own
   aria-pressed rather than a class of its own, so there is exactly one place
   the state lives and the picture cannot drift from what is announced.

   Sized off the 16px icon on the other side of the label so the two ends of
   the button balance: a 34x20 track is the same optical weight as that icon.
   The thumb travels the track's width minus its own diameter and the 2px of
   padding either side, which is why 12px moves it flush to the far edge and
   not a pixel further. */
.switch{
  flex:none;position:relative;display:inline-block;
  width:34px;height:20px;border-radius:var(--radius-pill);
  background:var(--surface-sunken);
  box-shadow:inset 0 0 0 1px var(--border-default);
  transition:background-color var(--dur-base) var(--ease-out),
             box-shadow var(--dur-base) var(--ease-out);
}
.switch__thumb{
  position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;
  background:var(--mist-300);
  transition:transform var(--dur-base) var(--ease-spring),
             background-color var(--dur-base) var(--ease-out);
}
[aria-pressed="true"] .switch{
  background:var(--jade-400);
  box-shadow:inset 0 0 0 1px var(--jade-400);
}
[aria-pressed="true"] .switch__thumb{
  background:var(--pine-900);
  transform:translateX(14px);
}
/* The button's own :hover already tints the label and the icon jade. On the
   switch that would fight the state colour, so it lifts the track a step
   instead - visible as a response, silent about which way it is set. */
.kit__toggle:hover .switch{box-shadow:inset 0 0 0 1px var(--border-inverse)}
.kit__toggle:hover[aria-pressed="true"] .switch{box-shadow:inset 0 0 0 1px var(--jade-300)}

/* The spring on the thumb is the one bit of motion here worth suppressing:
   it overshoots by design, and the track colour alone still says which way
   the switch is set. */
@media (prefers-reduced-motion:reduce){
  .switch,.switch__thumb{transition:none}
}
.kit__grid{
  list-style:none;margin:var(--space-8) 0 0;padding:0;
  display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-5);
}
.kit__tile{
  --kit-accent:var(--jade-400);
  --kit-font:var(--font-display);
  --kit-radius:var(--radius-sm);
  --kit-weight:700;
  --kit-track:var(--tracking-tight);
  display:flex;flex-direction:column;gap:var(--space-3);
}
.kit.is-loose .kit__tile{
  --kit-accent:var(--loose-accent);
  --kit-font:var(--loose-font);
  --kit-radius:var(--loose-radius);
  --kit-weight:var(--loose-weight);
  --kit-track:var(--loose-track);
}
.kit__label{
  font-family:var(--font-body);font-weight:700;font-size:var(--size-eyebrow);
  letter-spacing:var(--tracking-eyebrow);text-transform:uppercase;color:var(--mist-300);
}
.kit__art{
  display:flex;flex-direction:column;gap:var(--space-3);
  min-height:132px;padding:var(--space-4);
  background:var(--surface-sunken);
  border:1px solid var(--border-subtle);
  border-radius:var(--kit-radius);
  transition:border-radius var(--dur-base) var(--ease-out);
}
/* The shopfront is a sign, so it centres its mark and nothing else. */
.kit__art--sign{align-items:center;justify-content:center;min-height:132px}
/* Off the guidelines, half the tiles also drift out of alignment - the same
   thing that happens when six people lay six things out on six afternoons. */
.kit.is-loose .kit__tile:nth-child(even) .kit__art{align-items:center;text-align:center}

.kit__mark{
  font-family:var(--kit-font);font-weight:var(--kit-weight);
  letter-spacing:var(--kit-track);color:var(--kit-accent);
  font-size:var(--size-h4);line-height:1;
  transition:color var(--dur-base) var(--ease-out),
             letter-spacing var(--dur-base) var(--ease-out);
}
.kit__mark--big{font-size:var(--size-h2)}

.kit__bar{
  display:block;height:6px;width:100%;
  border-radius:var(--radius-pill);background:var(--night-600);
}
.kit__bar--short{width:58%}
.kit__bar--accent{background:var(--kit-accent);transition:background var(--dur-base) var(--ease-out)}
.kit__bar--btn{height:14px;width:44%;border-radius:var(--kit-radius);margin-top:auto}

.kit__explain{
  margin-top:var(--space-6);max-width:72ch;
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--mist-200);
}

@media (max-width:860px){
  .kit__grid{grid-template-columns:repeat(2,1fr)}
}
@media (max-width:480px){
  .kit__grid{grid-template-columns:1fr}
}

/* --------------------------------------------------------------------------
   The routes in - marketing.html
   Five ways a stranger reaches the site, and the ability to shut each one.
   The zero state is the page's whole argument, so it is reachable in five
   clicks and says so plainly rather than just going quiet.

   The line is the only thing that changes state visually. Label and caption
   keep their colour throughout: text a visitor can still read has to stay
   readable, and "closed" is carried by the dashed rule, the count and the
   pressed state of the control - not by making the words harder to see.
   ========================================================================== */
.reach__reset{flex:none}
.reach__ch{
  font-family:var(--font-body);font-weight:700;font-size:var(--size-sm);
  padding:var(--space-2) var(--space-5);border-radius:var(--radius-pill);
  background:transparent;border:1px solid var(--border-default);
  color:var(--text-body);cursor:pointer;
  transition:color var(--dur-fast) var(--ease-out),
             border-color var(--dur-fast) var(--ease-out),
             background var(--dur-fast) var(--ease-out);
}
.reach__ch:hover{color:var(--text-heading);border-color:var(--border-strongline)}
.reach__ch:focus-visible{outline:none;box-shadow:var(--focus-ring)}
.reach__ch[aria-pressed="true"]{
  background:var(--jade-400);border-color:var(--jade-400);color:var(--text-on-accent);
}
.reach__controls{display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-6)}

.reach__stage{
  display:grid;grid-template-columns:1fr minmax(180px,220px);
  gap:var(--space-8);align-items:center;margin-top:var(--space-8);
}
.reach__paths{list-style:none;margin:0;padding:0}
.reach__path{
  display:grid;grid-template-columns:1fr 96px;
  gap:var(--space-1) var(--space-5);align-items:center;
  padding-block:var(--space-4);
  border-bottom:1px solid var(--border-subtle);
}
.reach__label{
  grid-column:1;grid-row:1;
  font-family:var(--font-display);font-weight:700;font-size:var(--size-h4);
  letter-spacing:var(--tracking-tight);color:var(--text-heading);
}
.reach__who{
  grid-column:1;grid-row:2;
  font-size:var(--size-sm);line-height:var(--leading-snug);color:var(--text-body);
}
/* The route itself. Solid and jade when open, dashed and faint when shut. */
.reach__line{
  grid-column:2;grid-row:1 / 3;position:relative;
  height:0;border-top:2px dashed var(--border-default);
  transition:border-color var(--dur-base) var(--ease-out);
}
.reach__line::after{
  content:"";position:absolute;right:-1px;top:50%;
  width:7px;height:7px;border-radius:50%;
  transform:translateY(-50%);background:var(--border-default);
  transition:background var(--dur-base) var(--ease-out);
}
.reach__path.is-on .reach__line{border-top-style:solid;border-top-color:var(--jade-400)}
.reach__path.is-on .reach__line::after{background:var(--jade-400)}

.reach__site{
  display:flex;flex-direction:column;align-items:center;gap:var(--space-2);
  padding:var(--space-6) var(--space-5);text-align:center;
  background:var(--surface-raised);
  border:1px solid var(--border-inverse);border-radius:var(--radius-lg);
  transition:border-color var(--dur-base) var(--ease-out);
}
/* Nothing reaches it. The panel stops looking connected to anything. */
.reach.is-cut .reach__site{border-color:var(--border-default)}
.reach__site-mark{color:var(--jade-400);transition:color var(--dur-base) var(--ease-out)}
.reach.is-cut .reach__site-mark{color:var(--mist-400)}
.reach__site-label{
  font-family:var(--font-display);font-weight:700;font-size:var(--size-h4);
  letter-spacing:var(--tracking-tight);color:var(--text-heading);
}
.reach__count{
  font-size:var(--size-sm);color:var(--text-body);font-variant-numeric:tabular-nums;
}
.reach__explain{
  margin-top:var(--space-6);max-width:72ch;
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--mist-200);
}

@media (max-width:860px){
  .reach__stage{grid-template-columns:1fr;gap:var(--space-6)}
  .reach__path{grid-template-columns:1fr 64px}
  .reach__site{flex-direction:row;justify-content:center;gap:var(--space-4);text-align:left}
}
@media (max-width:480px){
  .reach__site{flex-direction:column;text-align:center}
}

/* --------------------------------------------------------------------------
   Proof frames - seo.html
   Screenshot slots for the success story. They are dashed while empty and
   labelled as empty, because a frame that looks finished but holds nothing
   reads as a chart the visitor failed to load. :has() turns the border solid
   once a real image is dropped in; browsers without it simply keep the dash.
   ========================================================================== */
/* Two across, not three. The graphs read against each other - same axis
   shape, same three months - so they belong on one row, and half a container
   each is nearly twice the width they had. The ranking screenshot takes the
   row underneath. */
.proof{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:clamp(24px,3vw,40px);margin-top:clamp(40px,5vw,64px);
}
.proof__item{margin:0}
.proof__frame{
  /* 16/9 matches the two graphs exactly, so they fill their frames rather than
     sitting in letterbox bars. The ranking shot overrides it below. */
  aspect-ratio:16 / 9;display:grid;place-items:center;overflow:hidden;
  position:relative;
  background:var(--surface-sunken);
  border:1px dashed var(--border-default);border-radius:var(--radius-md);
}
.proof__frame:has(img){border-style:solid;border-color:var(--border-subtle)}
/* contain, not cover: these are data. Cropping one to fill the frame would
   take an axis label or a search result off the edge, and the two graphs
   (16:9) and the search screenshot (1.27) do not share a shape anyway.

   max-width/max-height rather than width:height:100%. The frame is a grid with
   place-items:center, so its items size to content instead of stretching, and
   a percentage height there resolved against nothing useful - the 1.27 image
   came out 50px taller than the frame and overflow:hidden clipped the bottom
   result straight off. Capping both axes and letting the intrinsic ratio drive
   the rest cannot overflow whatever shape goes in. */
/* Absolutely positioned, the same way .device__screen img and
   .laptop__screen img are handled in styles.css.

   The frame takes its height from aspect-ratio, so a percentage height on a
   flow child is circular - it resolved to auto, the intrinsic 786x621 ratio
   took over, and the search screenshot rendered 262px tall inside a 223px
   frame with overflow:hidden clipping the bottom result clean off. inset:0
   against the padding box is definite, so object-fit does the letterboxing
   whatever shape goes in. */
.proof__frame img{
  position:absolute;inset:0;
  display:block;width:100%;height:100%;
  padding:var(--space-3);          /* inside the box; box-sizing is border-box */
  object-fit:contain;
}
.proof__ph{
  display:flex;flex-direction:column;align-items:center;gap:var(--space-2);
  font-size:var(--size-sm);color:var(--mist-300);text-align:center;
}
.proof__ph .ic{color:var(--mist-400)}
.proof__cap{margin-top:var(--space-4)}
.proof__stat{
  font-family:var(--font-display);font-weight:700;font-size:var(--size-h4);
  letter-spacing:var(--tracking-tight);color:var(--text-heading);
}
.proof__cap p + p{
  margin-top:var(--space-2);
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--text-body);
}

@media (max-width:860px){
  .proof{grid-template-columns:1fr;gap:var(--space-10)}
}

/* --------------------------------------------------------------------------
   Partner boxes - work.html
   Two screenshot slots for client sites, as social proof, with a heading and
   body under each.

   The frames follow the same rule as .proof on seo.html: dashed and labelled
   while empty, because a frame that looks finished but holds nothing reads as
   an image the visitor failed to load. :has() turns the border solid once a
   real screenshot is dropped in; browsers without it simply keep the dash.

   Two things are deliberate about the glow. It is jade at low alpha rather
   than a neutral shadow, because on the page ground a black shadow is
   invisible - there is nothing darker to cast onto. And it sits under a
   black shadow rather than replacing it: the black gives the card its weight,
   the jade gives it the lift off the ground.
   ========================================================================== */
/* Stacked, not side by side. Two columns put each screenshot at about 500px
   wide on a 1440 screen, which is too small to see what the page actually
   looks like - and looking at it is the entire job of this section. One column
   roughly doubles that. The trade is a taller section, which is the right way
   round: a visitor who has scrolled this far is here to look. */
.partners{
  display:grid;grid-template-columns:1fr;
  gap:clamp(32px,4vw,56px);margin-top:clamp(40px,5vw,64px);
}
.partners__item{margin:0}

.partners__card{
  display:block;height:100%;
  background:var(--surface-card);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-xl);
  padding:clamp(14px,1.4vw,18px);
  /* Weight from the black, lift from the jade. */
  box-shadow:0 18px 44px -26px rgba(0,0,0,.85),
             0 10px 40px -22px rgba(74,222,128,.20);
  transition:transform var(--dur-base) var(--ease-out),
             box-shadow var(--dur-base) var(--ease-out),
             border-color var(--dur-base) var(--ease-out);
}
/* Off the page on hover. translateZ keeps it on its own layer so the shadow
   redraw does not judder, and the scale is small on purpose - a screenshot
   that grows much past this starts to look like it is being zoomed rather
   than lifted. */
.partners__card:hover,
.partners__card:focus-within{
  transform:translateY(-8px) scale(1.015) translateZ(0);
  border-color:var(--border-inverse);
  box-shadow:0 40px 80px -32px rgba(0,0,0,.92),
             0 18px 64px -20px rgba(74,222,128,.38);
}

/* 21/10, not the 16/10 this was written for. The supplied screenshots are
   about 2.1:1 - full-width captures of wide pages - and at 16/10 cover was
   scaling them to fill the height and cutting roughly a quarter off the sides.
   On a screenshot of a web page that is the worst thing to lose: the edges are
   where the layout is. Matching the frame to the shots crops almost nothing.
   Re-measure this if the images are ever replaced with a different capture. */
.partners__frame{
  position:relative;
  aspect-ratio:21 / 10;display:grid;place-items:center;overflow:hidden;
  background:var(--surface-sunken);
  border:1px dashed var(--border-default);border-radius:var(--radius-md);
}
.partners__frame:has(img){border-style:solid;border-color:var(--border-subtle)}
.partners__shot{display:block;width:100%;height:100%;object-fit:cover;object-position:top center}

/* The second page, inset bottom left, and deliberately big enough to judge.
   It started at a quarter of the width on the reasoning that an inset past
   about a third stops being a corner detail and starts competing with the
   shot behind it. That is true of a decorative inset; it is the wrong trade
   here, because the job of this section is letting someone see the quality of
   the work, and a 290px thumbnail of a full page shows the shape of a layout
   and none of the craft in it. It is 38% now - clearly the second image, still
   clearly secondary.

   Its source was re-encoded at 1400px to go with the size. At the old 800px it
   would have been running at barely 1x on a retina screen, which would have
   made it larger and softer - the opposite of the point.

   It needs its own ring and shadow because it is a dark screenshot sitting on
   another dark screenshot - without them the two just merge into one image. */
.partners__inset{
  position:absolute;left:clamp(12px,1.6%,22px);bottom:clamp(12px,1.6%,22px);
  width:38%;height:auto;aspect-ratio:21 / 10;
  object-fit:cover;object-position:top center;
  border-radius:calc(var(--radius-md) - 2px);
  border:1px solid var(--border-inverse);
  /* Deeper than it was. A bigger panel sitting on a busy screenshot needs more
     separation under it, or its bottom edge disappears into the image. */
  box-shadow:0 14px 34px -10px rgba(0,0,0,.94);
}

.partners__cap{padding:var(--space-6) var(--space-2) var(--space-2)}
.partners__cap p{
  margin-top:var(--space-3);
  color:var(--text-body);line-height:var(--leading-body);
}

/* No column query any more - the grid is one column at every width. The inset
   grows back as the card narrows, because a fixed percentage of a phone-width
   card is too small to read as anything. Below 520 it is dropped: at that
   point a quarter of the width is a grey rectangle rather than evidence of a
   second page. */
@media (max-width:860px){
  .partners__inset{width:44%}
}
@media (max-width:520px){
  .partners__inset{display:none}
}

/* The lift is the one thing here that has to go: a card that jumps under the
   pointer is exactly what this query is for. The glow still brightens, so
   hover is still answered. */
@media (prefers-reduced-motion:reduce){
  .partners__card{transition:none}
  .partners__card:hover,
  .partners__card:focus-within{transform:none}
}

/* --------------------------------------------------------------------------
   The data report - work.html
   Imported from Animations/Example Data/index.html, where it was a standalone
   page. Everything is scoped under .gro-data so the component keeps its own
   look without reaching into the rest of the page, and so the two can be
   pulled apart again later.

   No charting library, same as before: the site ships no third-party JS. What
   is new is that the bars, the stat figures and the source rows are built by
   main.js (initDataReport) from one array per chart, so the printed figure and
   the drawn height come from the same number by construction rather than by
   somebody remembering to edit both.

   The figures stay readable without the picture - every bar carries its value
   and its month as real text, in order - so there is still no data table.

   --gro-mul multiplies every duration in here, which is the one knob for
   slowing the whole thing down or speeding it up.
   ========================================================================== */
.gro-data{
  --gro-accent:var(--jade-400);
  --gro-accent-strong:#22C55E;
  --gro-surface:var(--surface-card);
  --gro-surface-hover:#0F1815;
  --gro-border:rgba(74,222,128,.14);
  --gro-mul:1;
  display:flex;flex-direction:column;gap:clamp(16px,2vw,24px);
}

/* ---- The three headline figures --------------------------------------- */
.gro-stats{display:flex;flex-wrap:wrap;gap:clamp(12px,1.6vw,20px)}
.gro-stat{
  flex:1 1 240px;min-width:0;background:var(--gro-surface);
  border:1px solid var(--gro-border);border-radius:var(--radius-lg);
  padding:clamp(20px,2.4vw,30px);display:flex;flex-direction:column;gap:10px;cursor:default;
  transition:border-color 240ms cubic-bezier(.22,1,.36,1),
             transform 240ms cubic-bezier(.22,1,.36,1),
             background 240ms cubic-bezier(.22,1,.36,1);
}
.gro-stat:hover,.gro-stat.is-active{
  border-color:rgba(74,222,128,.44);transform:translateY(-4px);background:var(--gro-surface-hover);
}
.gro-stat-value{
  font-family:var(--font-display);font-weight:var(--weight-display);
  letter-spacing:var(--tracking-display);line-height:var(--leading-display);
  font-size:clamp(40px,6vw,62px);color:var(--gro-accent);font-variant-numeric:tabular-nums;
}
.gro-stat-label{font-size:var(--size-sm);line-height:1.5;color:var(--text-body);max-width:24ch}
/* Reserved height, so the card does not jump when the exact figure swaps in. */
.gro-stat-detail-wrap{min-height:34px;display:flex;align-items:flex-end}
.gro-stat-detail{font-size:12.5px;line-height:1.45;color:var(--text-muted);font-variant-numeric:tabular-nums}

/* ---- Shared card ------------------------------------------------------- */
.gro-card{
  background:var(--gro-surface);border:1px solid var(--gro-border);border-radius:var(--radius-lg);
  padding:clamp(20px,2.6vw,34px);display:flex;flex-direction:column;gap:clamp(16px,2vw,22px);
}
.gro-card-head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px}
/* Heading and copy. Two versions, and the difference matters: --intro is the
   half of a .gro-card-head that shares a row with the legend, so it grows and
   wraps against it. --titles is the same block standing alone at the top of a
   card. Using --intro there would read its flex-basis as a height, because the
   card itself is a column, and open 320px of nothing under the copy. */
.gro-card-intro{flex:1 1 320px;min-width:0;display:flex;flex-direction:column;gap:10px}
.gro-card-titles{display:flex;flex-direction:column;gap:10px}
.gro-h2{
  margin:0;font-family:var(--font-display);font-weight:var(--weight-display);
  letter-spacing:var(--tracking-tight);font-size:clamp(20px,2.4vw,26px);color:var(--mist-100);
}
.gro-card-copy{
  margin:0;max-width:62ch;font-size:var(--size-sm);
  line-height:var(--leading-body);color:var(--text-body);text-wrap:pretty;
}

.gro-legend{display:flex;flex-direction:column;gap:10px;align-items:flex-start}
.gro-legend-row{display:flex;align-items:center;gap:16px;font-size:12.5px;color:var(--text-body)}
.gro-legend-item{display:inline-flex;align-items:center;gap:7px}
.gro-legend-swatch{width:10px;height:10px;border-radius:3px}
.gro-legend-swatch--before{background:rgba(242,245,243,.22)}
.gro-legend-swatch--after{background:var(--gro-accent)}

/* The one line that changes on hover. Height is fixed so hovering along a row
   of bars does not walk the chart up and down the page. */
.gro-readout{
  min-height:40px;display:flex;align-items:center;padding:10px 14px;border-radius:var(--radius-md);
  background:rgba(74,222,128,.06);border:1px solid rgba(74,222,128,.12);
  font-size:13.5px;color:var(--mist-100);font-variant-numeric:tabular-nums;
}

/* ---- Vertical bars, with the trend line over them ---------------------- */
.gro-chart-wrap{position:relative;padding-top:12px}
.gro-chart-bars{
  position:relative;height:clamp(200px,30vw,320px);display:flex;align-items:flex-end;
  gap:clamp(3px,1vw,14px);border-bottom:1px solid var(--border-subtle);
}
/* Drawn by animating a dash the length of the whole path from fully offset to
   nothing, which is why pathLength="1" is on the polyline. */
.gro-trendline{position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none}
.gro-trendline polyline{
  fill:none;stroke:rgba(242,245,243,.5);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;
  vector-effect:non-scaling-stroke;stroke-dasharray:1 1;stroke-dashoffset:1;
  transition:stroke-dashoffset calc(1700ms * var(--gro-mul)) cubic-bezier(.33,.9,.4,1) calc(420ms * var(--gro-mul));
}
.gro-trendline polyline.is-drawn{stroke-dashoffset:0}

.gro-month{
  flex:1 1 0;min-width:0;height:100%;display:flex;flex-direction:column;justify-content:flex-end;
  position:relative;cursor:default;
}
.gro-month-value{
  flex:0 0 auto;text-align:center;font-size:clamp(9px,1.15vw,12.5px);font-weight:600;
  color:var(--mist-200);padding-bottom:5px;font-variant-numeric:tabular-nums;white-space:nowrap;
}
.gro-bar{
  flex:0 0 auto;height:0%;min-height:2px;border-radius:6px 6px 0 0;border-top:0 solid transparent;
  transition:height calc(1050ms * var(--gro-mul)) cubic-bezier(.22,1,.36,1) var(--bar-delay,0ms),
             filter 160ms cubic-bezier(.22,1,.36,1),
             box-shadow 160ms cubic-bezier(.22,1,.36,1);
}
.gro-bar--before{background:rgba(242,245,243,.22)}
.gro-bar--default{background:linear-gradient(180deg,var(--gro-accent),var(--gro-accent-strong))}
/* August is hatched rather than solid because it is a partial month. It is the
   tallest bar and the one most likely to be misread as a finished one. */
.gro-bar--aug{
  background:repeating-linear-gradient(45deg,rgba(74,222,128,.95) 0 7px,rgba(74,222,128,.28) 7px 15px);
  border-top:2px solid var(--gro-accent);
}
.gro-month:hover .gro-bar,.gro-month.is-active .gro-bar{
  filter:brightness(1.14);box-shadow:0 0 0 1px rgba(74,222,128,.6);
}

/* Its own flex row under the plot, with the same gap, so each label sits under
   the bar it belongs to without the labels being inside the plot's height. */
.gro-chart-labels{display:flex;gap:clamp(3px,1vw,14px);padding-top:10px}
.gro-chart-labels span{
  flex:1 1 0;min-width:0;text-align:center;font-size:clamp(9px,1.15vw,12.5px);
  color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}

/* The three things about these numbers that a reader could otherwise get
   wrong. They are in the chart card on purpose rather than in a footnote. */
.gro-caveats{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));
  gap:14px;padding-top:16px;border-top:1px solid var(--border-subtle);
}
.gro-caveat{display:flex;flex-direction:column;gap:5px}
.gro-caveat-title{
  font-size:12px;font-weight:var(--weight-display);letter-spacing:.14em;
  text-transform:uppercase;color:var(--gro-accent);
}
.gro-caveat-body{font-size:13px;line-height:1.55;color:var(--text-muted)}

/* ---- Horizontal bars, for the breakdown -------------------------------- */
.gro-sources{display:flex;flex-direction:column;gap:4px}
.gro-source-row{
  display:flex;align-items:center;gap:clamp(10px,1.6vw,20px);padding:9px 10px;
  border-radius:var(--radius-sm);cursor:default;
  transition:background 160ms cubic-bezier(.22,1,.36,1);
}
.gro-source-row:hover,.gro-source-row.is-active{background:rgba(242,245,243,.045)}
.gro-source-label{
  flex:0 0 auto;width:clamp(52px,8vw,76px);font-size:13.5px;font-weight:600;color:var(--mist-100);
}
.gro-source-track{
  flex:1 1 auto;min-width:0;height:12px;border-radius:var(--radius-pill);
  background:var(--surface-sunken);overflow:hidden;
}
/* min-width keeps Email, which is two sessions in a year, from vanishing. */
.gro-source-fill{
  height:100%;min-width:3px;border-radius:var(--radius-pill);width:0%;
  background:linear-gradient(90deg,var(--gro-accent-strong),var(--gro-accent));
  transition:width calc(1100ms * var(--gro-mul)) cubic-bezier(.22,1,.36,1) var(--bar-delay,0ms);
}
.gro-source-printed{
  flex:0 0 auto;width:clamp(92px,13vw,128px);text-align:right;font-size:13px;font-weight:600;
  color:var(--mist-200);font-variant-numeric:tabular-nums;
}

.gro-fineprint{
  margin:0;padding-top:var(--space-5);border-top:1px solid var(--border-subtle);
  font-size:var(--size-xs);line-height:var(--leading-body);color:var(--text-muted);max-width:80ch;
}

@media (max-width:700px){
  .gro-caveats{padding-top:12px}
  .gro-source-row{gap:10px;padding:8px 6px}
}

/* Bars and fills are sized by JS setting an inline height/width; with the
   transitions off they simply appear at their final size. */
@media (prefers-reduced-motion:reduce){
  .gro-stat,.gro-bar,.gro-trendline polyline,.gro-source-fill{
    transition:none !important;animation:none !important;
  }
}

/* --------------------------------------------------------------------------
   Testimonial - work.html
   A single pull quote with the person under it.

   Centred, and the only centred composition on the site - asked for, and it
   suits a single pull quote in a way it would not suit a section of copy. The
   card is narrowed as well as centred, because centring a full-width card does
   nothing: it is the narrowing that makes it read as an object on the page
   rather than another band of content.

   The quote measure is tighter than it would be left-aligned (36ch against
   46ch). Centred text is harder to track back to the start of the next line,
   so the lines have to be shorter to stay comfortable.

   The glow is the same one the partner boxes use, at about half strength and
   with no hover state - this is a thing to read, not a thing to click.
   ========================================================================== */
.quote{
  margin:clamp(32px,4vw,48px) auto 0;
  max-width:820px;text-align:center;
  background:var(--surface-card);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-2xl);
  padding:clamp(32px,4.5vw,64px);
  box-shadow:0 18px 44px -26px rgba(0,0,0,.85),
             0 10px 40px -24px rgba(74,222,128,.14);
}
.quote__text{
  margin:0 auto;max-width:36ch;text-wrap:balance;
  font-family:var(--font-display);font-weight:var(--weight-display);
  font-size:clamp(1.25rem,2.4vw,1.875rem);
  line-height:var(--leading-snug);letter-spacing:var(--tracking-tight);
  color:var(--text-heading);
}

/* The person, under the quote. flex-wrap so a long name and role drop below
   the portrait on a phone rather than squeezing it. */
/* Stacked and centred under the quote, with no rule above it: a divider
   across a centred composition cuts it in half rather than joining it. The
   space does the separating. */
.quote__by{
  display:flex;flex-direction:column;align-items:center;gap:var(--space-4);
  margin-top:clamp(28px,3.5vw,44px);
}
/* Dashed and empty until a real portrait is dropped in, the same rule the
   screenshot frames follow: a circle that looks finished but holds nothing
   reads as an avatar that failed to load. :has() turns it solid on its own. */
.quote__avatar{
  flex:none;width:56px;height:56px;border-radius:50%;
  display:grid;place-items:center;overflow:hidden;
  background:var(--surface-sunken);
  border:1px dashed var(--border-default);color:var(--mist-400);
}
.quote__avatar:has(img){border-style:solid;border-color:var(--border-subtle)}
.quote__avatar img{display:block;width:100%;height:100%;object-fit:cover}
.quote__name{display:block;font-weight:700;color:var(--text-heading)}
.quote__role{display:block;margin-top:2px;font-size:var(--size-sm);color:var(--text-on-dark-muted)}

/* --------------------------------------------------------------------------
   Who we are
   The .who rules moved to styles.css when index.html needed them - index.html
   does not load this file. Nothing else changed; about.html and contact.html
   both load styles.css, so their markup is untouched.
   ========================================================================== */

@media (prefers-reduced-motion:reduce){
  .kit__art,.kit__mark,.kit__bar--accent,
  .reach__line,.reach__line::after,
  .reach__site,.reach__site-mark{transition:none}
}

/* --------------------------------------------------------------------------
   The four disciplines - seo.html#kinds

   A 2x2 of what the word "SEO" actually covers. Ordinary page ground under
   them, as on the landing page, with the cards carrying the contrast - the
   sections either side of this one alternate pine and page, and a third
   treatment here would make the run of them look like four unrelated pages.

   These are not links. The hover is there because the brief asked for it and
   because it makes a dense grid easier to read - it tells you which cell your
   eye is in. Only the local cell contains a link, and that is a .textlink
   inside the card rather than the card itself, so nothing here pretends to be
   clickable when it is not.
   ========================================================================== */
.quads{
  display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
  gap:var(--gutter);margin-top:clamp(40px,5vw,64px);
}
.quad{
  display:flex;flex-direction:column;
  background:var(--surface-card);
  border:1px solid var(--border-subtle);
  border-radius:var(--radius-lg);
  padding:clamp(24px,2.4vw,36px);
  transition:transform var(--dur-base) var(--ease-out),
             border-color var(--dur-base) var(--ease-out),
             box-shadow var(--dur-base) var(--ease-out);
}
/* jade-400 is the brand green proper - the same one the eyebrows and the
   primary button use. The second ring in the shadow is what reads as an
   outline; a 2px border instead would shift the text by a pixel on hover. */
.quad:hover{
  /* --lift is the one that lands: these cards carry .reveal, and .js
     .reveal.is-in outranks this rule on transform. See the note on that rule
     in styles.css. transform is kept for a .quad used without .reveal. */
  transform:translateY(-4px);
  --lift:translateY(-4px);
  border-color:var(--jade-400);
  box-shadow:0 0 0 1px rgba(74,222,128,.38), var(--shadow-md);
}
.quad__icon{
  width:46px;height:46px;border-radius:var(--radius-md);
  background:var(--surface-accent-soft);color:var(--jade-300);
  display:grid;place-items:center;margin-bottom:var(--space-5);
}
.quad__title{color:var(--text-heading)}
.quad__what{
  margin-top:var(--space-4);
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--text-body);
}
/* Small caps label rather than another heading: the card already has an h3 and
   a second one would put noise in the document outline for a list of five. */
.quad__label{
  margin-top:var(--space-6);
  font-family:var(--font-body);font-weight:var(--weight-display);
  font-size:var(--size-xs);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--text-muted);
}
.quad__list{list-style:none;padding:0;margin:var(--space-4) 0 0;display:grid;gap:var(--space-3)}
.quad__list li{
  display:grid;grid-template-columns:14px 1fr;gap:var(--space-3);align-items:start;
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--text-body);
}
.quad__list .ic{color:var(--jade-400);margin-top:5px}
/* margin-top:auto pins it to the bottom however uneven the lists are */
.quad__more{margin-top:auto;padding-top:var(--space-6)}

@media (max-width:820px){
  .quads{grid-template-columns:1fr}
}
@media (prefers-reduced-motion:reduce){
  .quad{transition:border-color var(--dur-base) var(--ease-out)}
  .quad:hover{transform:none;--lift:none}
}

/* --------------------------------------------------------------------------
   Banner section headings, and a three-up steps row

   .svc-intro caps itself at 640px, which is right for a heading sitting above
   a grid of its own. It is wrong where the heading is the point: at
   .display-2's 4rem cap inside a 640px column, a section heading used barely
   half the container.

   .svc-headline is the landing page's device brought across - the same one
   .growth__headline and .grant__headline use there, and the same 7.4vw term,
   so a heading here scales exactly as one there does.

   The 6.25rem cap is measured, not chosen. In Space Grotesk bold at the
   1136px container: "SEO is four jobs, not one." is 11.1x the font size wide,
   so it fills the line at 102px; "What SEO actually is." is 9.4x, filling at
   121px. 100px puts the first at 98% of the line and the second at 83%, both
   on one line. Push the cap higher and the longer one wraps.

   No text-wrap:balance - .svc-intro sets it on .display-2, and balance
   shortens lines, which is the opposite of filling the width. That is why
   this is its own class rather than a modifier on the existing one.

   Longer headings than these two will wrap to two lines, which is fine and is
   what the landing page's growth headline does. Re-measure before assuming a
   new string fits on one.
   ========================================================================== */
.svc-banner{max-width:none}
.svc-headline{
  margin-top:var(--space-4);
  font-size:clamp(2.5rem,7.4vw,6.25rem);
  line-height:var(--leading-display);
  letter-spacing:var(--tracking-display);
}
.svc-banner .lead{margin-top:var(--space-6);max-width:60ch}

/* .steps is built for the four-step process rows. Three steps in that grid
   leave a column of nothing against the right margin. */
.steps--3{grid-template-columns:repeat(3,minmax(0,1fr))}
@media (max-width:900px){
  .steps--3{grid-template-columns:1fr}
}

/* --------------------------------------------------------------------------
   Stacked hero heading - seo.html

   The base hero h1 caps at 5.75rem/92px, which left it SMALLER than the 100px
   .svc-headline sections below it: the page's biggest type was in the middle
   of it rather than at the top. This puts the hierarchy back the right way up.

   Measured at the 1136px container: "Get ranked." is 5.17x the font size
   wide and "Get found." 4.78x, so either line alone would only fill the
   container at about 220px. That is far past impactful and into shouting, so
   the cap is 10rem/160px - comfortably above the 100px headings underneath,
   and each line sits at roughly three quarters of the measure.

   Overrides the 16ch cap and the balance from .svc-hero__copy .display-1:
   the break is deliberate here, so nothing should be rewrapping it. Same
   specificity as that rule and later in the file, which is what carries it.
   ========================================================================== */
/* --title-cap because the right size depends on the longest of the two
   sentences, which differs per page. seo.html takes the 10rem default; about
   sets 6.75rem, the size at which "GRO isn't your agency." still fits one
   line (10.25x the font size against a 1136px container fills at 111px). */
.svc-hero__copy .svc-hero__title{
  max-width:none;
  font-size:clamp(3rem,11.5vw,var(--title-cap,10rem));
  text-wrap:wrap;
}
.svc-hero__title .jade{display:block}

/* --------------------------------------------------------------------------
   SEO against paid advertising - seo.html#benefits

   A real table, styled rather than replaced. The SEO column is tinted so the
   eye can follow one side down the page without reading every cell; the ads
   column is left plain rather than made to look bad, because the section only
   works if it reads as a comparison.
   ========================================================================== */
.vs-wrap{margin-top:clamp(40px,5vw,64px)}
.vs{
  width:100%;border-collapse:collapse;
  font-size:var(--size-sm);line-height:var(--leading-body);
}
.vs th,.vs td{text-align:left;vertical-align:top;padding:var(--space-5) var(--space-6)}

/* Column heads. The first cell is empty by design - the row headers say what
   each line measures, so a label above them would only repeat itself. */
.vs thead th{
  font-family:var(--font-body);font-weight:var(--weight-display);
  font-size:var(--size-xs);letter-spacing:var(--tracking-eyebrow);
  text-transform:uppercase;color:var(--text-muted);
  padding-bottom:var(--space-4);
  border-bottom:1px solid var(--border-default);
}
.vs thead th.vs__seo{color:var(--jade-400)}
.vs__hide{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}

.vs tbody tr{border-bottom:1px solid var(--border-subtle)}
.vs tbody tr:last-child{border-bottom:0}
.vs tbody th{
  font-family:var(--font-display);font-weight:var(--weight-display);
  font-size:var(--size-sm);letter-spacing:var(--tracking-tight);
  color:var(--text-heading);width:26%;
}
.vs tbody td{color:var(--text-body)}
/* The tint runs the full height of the column, which is what makes it read as
   a column rather than as seven separately highlighted cells. */
.vs .vs__seo{background:var(--surface-accent-soft);color:var(--mist-100)}
.vs tbody tr:first-child .vs__seo{border-radius:var(--radius-md) var(--radius-md) 0 0}
.vs tbody tr:last-child .vs__seo{border-radius:0 0 var(--radius-md) var(--radius-md)}

.vs__foot{
  margin-top:var(--space-8);max-width:68ch;
  font-size:var(--size-sm);line-height:var(--leading-body);color:var(--text-muted);
}

/* Stacked below 760px. A seven-row, three-column table cannot hold its shape
   on a phone, and side-scrolling a comparison defeats the comparison - you can
   only ever see one side. Each row becomes a small block: the measure as a
   heading, then the two answers labelled by data-col. The roles in the markup
   are what keep this a table to a screen reader once display:block lands. */
@media (max-width:760px){
  .vs,.vs tbody,.vs tr,.vs th,.vs td{display:block;width:auto}
  .vs thead{display:none}
  .vs tbody tr{
    border:1px solid var(--border-subtle);border-radius:var(--radius-md);
    padding:var(--space-5);margin-bottom:var(--space-4);
  }
  .vs tbody th{width:auto;padding:0 0 var(--space-4);font-size:var(--size-body)}
  .vs tbody td{padding:var(--space-3) 0 0}
  .vs tbody td::before{
    content:attr(data-col);display:block;margin-bottom:2px;
    font-family:var(--font-body);font-weight:var(--weight-display);
    font-size:var(--size-xs);letter-spacing:var(--tracking-eyebrow);
    text-transform:uppercase;color:var(--text-muted);
  }
  .vs .vs__seo{
    background:transparent;border-radius:0;
    border-left:2px solid var(--jade-400);padding-left:var(--space-4);
  }
  .vs .vs__seo::before{color:var(--jade-400)}
}

/* ---- The ranking screenshot, full width ---------------------------------
   A fixed height rather than a ratio. The source is 786x621; a frame the full
   width of the container would either be enormously tall or upscale a small
   screenshot well past the point its text stays readable. Capping the frame at
   900px and letting object-fit centre the image shows it at roughly native
   size, which is as legible as it gets.
   ------------------------------------------------------------------------ */
.proof__item--wide{grid-column:1 / -1}
.proof__item--wide .proof__frame{
  aspect-ratio:auto;height:clamp(320px,44vw,640px);
  max-width:900px;margin-inline:auto;
}
.proof__item--wide .proof__cap{max-width:900px;margin-inline:auto}

@media (max-width:860px){
  .proof__item--wide .proof__frame{height:auto;aspect-ratio:4 / 3}
}

/* --------------------------------------------------------------------------
   Mid-page hook - seo.html#hook

   One centred question and one button. The heading uses .svc-headline like
   every other section head on the page, so it does not read as a different
   kind of thing - but it opts back into text-wrap:balance, which the base
   class deliberately turns off. That rule exists to stop balance shortening
   lines that are meant to fill the container; here the block is centred and
   short, and evening the lines is exactly what it should do.
   ========================================================================== */
.svc-hook__inner{display:grid;justify-items:center;text-align:center}
.svc-hook .svc-headline{margin-top:0;max-width:22ch;text-wrap:balance}
.svc-hook .btn{margin-top:clamp(28px,3.5vw,44px)}

@media (max-width:560px){
  .svc-hook .btn{width:100%}
}

/* ==========================================================================
   Work page - "where to next" four across
   Scoped to #next, and living in this file rather than styles.css, because
   both are ways of keeping it off index.html: that page uses the same
   .svc-grid two across and does not load this stylesheet at all.

   The card padding and the gap come down with the column count. At four
   across on a 1136 container the default 24px gutter and 32px card padding
   leave about 196px of usable width per card, which is not enough for the
   art panel and a paragraph under it. Trimming both gets it back to ~230px.
   ========================================================================== */
#next .svc-grid{
  grid-template-columns:repeat(4,minmax(0,1fr));
  gap:clamp(14px,1.4vw,22px);
}
#next .svc-grid .card{padding:clamp(14px,1.1vw,18px)}
#next .svc-grid .card p{font-size:var(--size-sm)}

/* Service pages show the other three, not all four, so they get a column back.
   Declared before the media queries below on purpose: those are bare
   `#next .svc-grid` at the same specificity as this, so if this sat after them
   it would win inside them too and the grid would never step down. */
#next .svc-grid--3{grid-template-columns:repeat(3,minmax(0,1fr))}

/* Two across before the fourth column squeezes the copy into a column of
   single words. */
@media (max-width:1100px){
  #next .svc-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gutter)}
  #next .svc-grid .card{padding:var(--space-8)}
}
/* Three cards hold their row further down than four do - and dropping to two
   would leave the third on a line of its own, which the 2x2 above never does.
   Bounded below so it hands back to the two-across rule, not to the stack. */
@media (max-width:1100px) and (min-width:901px){
  #next .svc-grid--3{grid-template-columns:repeat(3,minmax(0,1fr))}
}
/* Restating the one-column rule from styles.css on purpose. That rule is a
   bare .svc-grid, so the id selector above outranks it at every width and the
   stack would never happen without this. */
@media (max-width:780px){
  #next .svc-grid{grid-template-columns:1fr}
}
