/* ============================================================================
   "How it works" — two timeline directions, startup-program page

   Namespaced under .hw so nothing leaks into Webflow's site-wide stylesheet,
   same containment discipline as .sp-bento. Colours, type and the isometric
   primitives come from css/site.css :root and css/bento.css.

   Ordering matters: the @media blocks live at the very END of this file.
   Rules appended after them would silently win over the breakpoint overrides.
   ========================================================================== */

.hw {
  /* The MEASURED brand ramp: #fdd07b -> #f58de3 -> #007eef with a 31% midpoint.
     Deliberately not --sp-brand, which is the older 55%-midpoint version — see
     "The measured language" in NOTES.md. */
  --hw-b0: #fdd07b;
  --hw-b1: #f58de3;
  --hw-b2: #007eef;
  --hw-brand-h: linear-gradient(90deg, var(--hw-b0) 0%, var(--hw-b1) 31%, var(--hw-b2) 100%);
  --hw-brand-v: linear-gradient(180deg, var(--hw-b0) 0%, var(--hw-b1) 31%, var(--hw-b2) 100%);
  --hw-brand-d: linear-gradient(150deg, var(--hw-b0) 0%, var(--hw-b1) 31%, var(--hw-b2) 100%);

  /* the pale ramp, same 31% midpoint */
  --hw-w0: #fff6e6;
  --hw-w1: #feeafa;
  --hw-w2: #cfe7fc;

  --hw-ring: var(--semantic--c-stroke);
  --hw-line: var(--primitive--c-neutral-300);
  --hw-tick: var(--primitive--c-neutral-500);
  --hw-ink: var(--semantic--c-foreground);
  --hw-ink-muted: var(--semantic--c-foreground-muted);
  --hw-ground: var(--primitive--c-neutral-100);

  /* Quint out — everything that responds to the user. 500ms, per the spec. */
  --hw-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --hw-dur: 0.5s;

  background-color: var(--hw-ground);
  padding: 4.5rem 3.75rem;
  color: var(--hw-ink);
  font-family: var(--font--saans);
}

/* ---------- shared type ---------- */

/* Type lives in css/section-title.css — the one copy shared with
   .sp-bento-title, .wf-title and .sp-title. Only spacing is ours; 2rem is the
   same heading-to-content gap .sp-bento-head and .wf-head hand out. */
.hw-title {
  margin: 0 0 2rem;
}

/* the 4x4 square + mono caption lockup, matching .c-tag on the live page */
.hw-tag {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.625rem;
}

.hw-tag::before {
  content: "";
  flex: 0 0 auto;
  width: 4px;
  height: 4px;
  background-color: var(--hw-ink);
}

.hw-tag--ghost::before {
  background-color: transparent;
  box-shadow: inset 0 0 0 1px var(--primitive--c-blue);
}

.hw-cap,
.hw-mono {
  font-family: var(--font--saans-mono);
  font-size: 0.6875rem;
  font-weight: 500;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--hw-ink-muted);
}

.hw-cap {
  color: var(--hw-ink);
}

.hw-h3 {
  margin: 0;
  font-family: var(--font--saans);
  font-size: 1.0625rem;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--hw-ink);
}

.hw-p {
  margin: 0.5rem 0 0;
  max-width: 46ch;
  font-size: 0.9375rem;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--hw-ink-muted);
}

/* white surface, square corners, hairline ring — the bento's .sp-tile */
.hw-card {
  display: flex;
  flex-direction: column;
  padding: 1.25rem;
  background-color: var(--primitive--c-white);
  /* the hairline stays put on hover - deliberately. Same call as .sp-tile in
     bento.css: everything that reacts to hover lives inside the box, and
     darkening its own border on top of that read as heavy. */
  box-shadow: inset 0 0 0 0.75px var(--hw-ring);
}

/* ============================================================================
   DIRECTION A — "The runway"

   One horizontal month axis, 0 to 12. Every mark is placed by month:
   left: calc(var(--m) / 12 * 100%). Nothing is eyeballed, so the M6 tick, the
   end of the gradient bar and the seam between the two cards are the same x by
   construction rather than by adjustment.
   ========================================================================== */

.hw-runway {
  /* one geometry, declared once. Every mark on the axis is placed off these
     two values plus its month, so nothing here is eyeballed. */
  --hw-base: 1.25rem;   /* the baseline, measured up from the axis floor */
  --hw-barh: 10px;      /* thickness of the free-stretch bar */
}

.hw-axis {
  position: relative;
  height: 4.75rem;
}

/* the baseline: a hairline the full 12 months */
.hw-axis-rule {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--hw-base);
  height: 1px;
  background-color: var(--hw-line);
}

/* one tick per month, 1px, hanging BELOW the rule */
.hw-tick {
  position: absolute;
  left: calc(var(--m) / 12 * 100%);
  bottom: calc(var(--hw-base) - 0.375rem);
  width: 1px;
  height: 0.375rem;
  background-color: var(--hw-line);
}

.hw-tick--major {
  bottom: calc(var(--hw-base) - 0.625rem);
  height: 0.625rem;
  background-color: var(--hw-tick);
}

/* month labels sit under their tick */
.hw-axis-label {
  position: absolute;
  bottom: 0;
  left: calc(var(--m) / 12 * 100%);
  transform: translateX(-50%);
  white-space: nowrap;
}

.hw-axis-label--start { transform: translateX(0); }
.hw-axis-label--end { transform: translateX(-100%); }

/* the free stretch: months 0 to 6, the measured brand ramp, static.
   In the source language the saturated surfaces never animate — only pale
   light moves across them, which is what ::after does below. */
.hw-bar {
  position: absolute;
  left: 0;
  width: calc(6 / 12 * 100%);
  bottom: var(--hw-base);
  height: var(--hw-barh);
  overflow: hidden;
  background-image: var(--hw-brand-h);
}

/* a narrow light travelling the free stretch. Background loops are LINEAR in
   this language — Quint is reserved for things responding to the user. */
.hw-bar::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    90deg,
    rgba(255, 255, 255, 0) 35%,
    rgba(255, 255, 255, 0.72) 50%,
    rgba(255, 255, 255, 0) 65%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  /* Resting position == the loop's 0% keyframe, so starting causes no jump.
     -130%/130% rather than the obvious -100%/100%: at a background-size of
     220% a position of P% puts the image's left edge at -1.2*P/100 of the
     host, so the highlight band (35-65% of the image) is only fully off the
     host past |P| = 119.2. At 110% its tail was still 11% inside, and the
     wrap blinked. At 130% both ends render the same empty frame. */
  background-position: -130% 0;
  animation: hw-sheen 3.2s linear infinite;
  animation-play-state: paused;
}

/* gating pauses rather than removes: removing restarts the sweep from its
   first frame, so an observer toggling near its threshold makes it jump */
.hw-bar.is-running::after {
  animation-play-state: running;
}

@keyframes hw-sheen {
  from { background-position: -130% 0; }
  to   { background-position: 130% 0; }
}

/* The reveal wipe — one pass, Quint, triggered by arriving on screen.

   The clip is gated on .is-armed, which JS adds only once it has an observer
   watching. Without that guard the bar starts clipped and STAYS clipped in
   every situation where the observer never fires — JS blocked, an old engine,
   or a background tab, where IntersectionObserver is frozen outright. The
   resting state has to be the finished one; the wipe is the enhancement. */
.hw-runway.is-armed:not(.is-revealed) .hw-bar {
  clip-path: inset(0 100% 0 0);
}

.hw-runway.is-armed.is-revealed .hw-bar {
  clip-path: inset(0 0 0 0);
  transition: clip-path 0.9s var(--hw-ease);
}

/* the paid stretch: months 6 to 12, hairline only, running through the
   vertical middle of the bar it continues from */
.hw-tail {
  position: absolute;
  left: calc(6 / 12 * 100%);
  right: 0.5rem;
  /* -0.5px so the 1px hairline is centred on the bar it continues from,
     not sitting half a pixel above it */
  bottom: calc(var(--hw-base) + var(--hw-barh) / 2 - 0.5px);
  height: 1px;
  background-color: var(--hw-tick);
}

.hw-tail::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: 6px;
  height: 6px;
  border-top: 1px solid var(--hw-tick);
  border-right: 1px solid var(--hw-tick);
  transform: translate(0, -50%) rotate(45deg);
}

/* the M1 annotation: a dashed riser and one mono label. The only note on the
   axis, because it is the only decision the reader has to make. */
.hw-riser {
  position: absolute;
  left: calc(1 / 12 * 100%);
  bottom: calc(var(--hw-base) + var(--hw-barh));
  height: 1.75rem;
  border-left: 1px dashed var(--primitive--c-blue);
  opacity: 0.75;
}

.hw-riser-label {
  position: absolute;
  left: calc(1 / 12 * 100%);
  bottom: calc(var(--hw-base) + var(--hw-barh) + 1.75rem + 0.3rem);
  transform: translateX(-50%);
  white-space: nowrap;
  color: var(--primitive--c-blue);
}

/* a hairline dropping from the M6 tick into the seam between the two cards */
.hw-drop {
  position: relative;
  height: 1.25rem;
}

.hw-drop::before {
  content: "";
  position: absolute;
  left: calc(6 / 12 * 100%);
  top: 0;
  bottom: 0;
  width: 1px;
  background-color: var(--primitive--c-neutral-400);
}

/* Two cards, and the seam between them is centred on 50% — the gap is split
   evenly out of each column rather than added between them, so the M6 tick,
   the end of the bar and the seam all land on the same x. */
.hw-cards {
  display: grid;
  grid-template-columns: calc(50% - 0.25rem) calc(50% - 0.25rem);
  gap: 0.5rem;
}

.hw-card-num {
  margin-top: 1.5rem;
}

/* the copy sits on the floor of the tile, so both cards agree on a baseline
   however much text each one carries */
.hw-card-foot {
  margin-top: auto;
  padding-top: 1.75rem;
}

.hw-num {
  display: block;
  font-family: var(--font--saans);
  font-size: 2.5rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: -0.035em;
  color: var(--hw-ink);
  font-variant-numeric: tabular-nums;
}

.hw-num-unit {
  display: block;
  margin-top: 0.5rem;
  font-family: var(--font--saans-mono);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--hw-ink-muted);
}

/* ============================================================================
   DIRECTION B — "The vertical timeflow"

   Three moments read top to bottom, but only TWO of them are on the rail. The
   opt-out is an aside about the programme, not a stage of it, so it hangs off
   the rail as a bubble and the gradient runs straight past it.

   The art is generated by js/gen_how.py to the measured language — real 30deg
   isometry, a 1.41 black line, radial gradients only, the extrusion standing in
   for the shadow. It replaced a set of CSS-skewed rounded rectangles filled with
   a LINEAR gradient, which is a different language entirely.
   ========================================================================== */

.hw-flow {
  margin: 0;
  padding: 0;
  list-style: none;
}

.hw-step {
  display: grid;
  grid-template-columns: 1rem 5.5rem minmax(0, 1fr);
  align-items: start;
  /* site.css ships a Webflow/Relume default of `li { margin: 4px 0 8px }`.
     Adjacent margins collapse to 8px, which opened an 8px hole in the rail
     between every pair of steps — the segments are drawn edge to edge and have
     nothing to bridge a gap with. The same reset also ships
     `li { padding-left: 20px }`, which slid every row 20px right of the rail
     the ::before draws against the li's padding box — the nodes sat off the
     line and the bubble's leader stopped 20px short of it. */
  margin: 0;
  gap: 0 1.25rem;
  padding: 0 0 2.25rem;
}

.hw-step:last-child {
  padding-bottom: 0;
}

/* ---- the rail ----

   The rail is drawn on the STEP, not on the rail cell, and runs `bottom: 0` —
   i.e. to the step's own bottom edge, padding included, which is exactly the
   next step's top edge. Consecutive segments then meet by construction.

   It was on the cell first, with `bottom: -2.25rem` to reach across the gap.
   That only lines up while every cell is stretched to its row; the aside's cell
   hits its own min-height instead, and the rail came apart with a 16px hole in
   it right where the bubble hangs off. */

.hw-step {
  position: relative;
}

.hw-step::before {
  content: "";
  position: absolute;
  left: calc(0.5rem - 0.5px);
  top: 0.5rem;
  bottom: 0;
  width: 1px;
  /* Each segment carries a SLICE of the ramp rather than the whole thing, so
     the rail reads as one gradient running the length of the free stretch —
     the split is at #f58de3, the ramp's 31% colour. */
  background-image: linear-gradient(180deg, var(--hw-b0), var(--hw-b1));
}

/* the aside has no node, so its segment starts at the step's very top and the
   ramp runs straight past the bubble without a break */
.hw-step--aside::before {
  top: 0;
  background-image: linear-gradient(180deg, var(--hw-b1), var(--hw-b2));
}

/* the last step: the rail arrives at its node and stops there */
.hw-step:last-child::before {
  top: 0;
  bottom: calc(100% - 0.5rem);
  background-image: none;
  background-color: var(--hw-b2);
}

.hw-step-rail {
  position: relative;
  align-self: stretch;
  justify-self: center;
  width: 1px;
  min-height: 4.5rem;
}

.hw-step-node {
  position: absolute;
  left: 50%;
  top: 0.5rem;
  width: 7px;
  height: 7px;
  margin: -3px 0 0 -3px;
  background-color: var(--hw-ink);
}

/* ---- the counting columns ----

   Six plates, one per free month: the art states the number rather than
   illustrating it. Everything is painted from here — not one path in the SVG
   carries a fill or stroke attribute, because a CSS declaration beats an inline
   one and the old shield lost a debugging cycle to exactly that clash. */

.hw-art {
  width: 100%;
  transition: transform var(--hw-dur) var(--hw-ease);
}

.hw-art svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* white top surface, saturated extrusion — saturation carries role */
.hw-top {
  fill: var(--primitive--c-white);
  stroke: none;
}

.hw-ext {
  fill: var(--hw-ext-fill);
  stroke: none;
}

/* THE LINE IS 1.41 PIXELS, NOT 1.41 USER UNITS, and that is the one place this
   art departs from the measured spec on purpose.

   The measurement is "1.41 units on ~378-unit-wide art", i.e. 0.373% of the art
   width — which holds as a *ratio* only because the source renders at roughly
   its native size, so a unit is about a pixel and the line lands at ~1.4px. This
   column renders at ~7rem from a 464-unit viewBox, a scale of 0.24: keeping the
   ratio would draw the outline at 0.27px, which on a white plate is nothing at
   all. It was built that way first and the plates read as untouched silhouettes.

   vector-effect pins the stroke to the non-scaled space, so the geometry stays
   in the self-documenting 380-unit system AND the rendered hairline stays at the
   measured weight at every breakpoint. */
.hw-ln {
  fill: none;
  stroke: var(--primitive--c-black);
  stroke-width: 1.41;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* the repeat, stepped back up-left along the isometric axis. Same pinned
   stroke, so the dash pattern is in pixels too. */
.hw-ghost {
  fill: none;
  stroke: #9fb6de;
  stroke-width: 1.41;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 4 3.5;
}

.hw-step:hover .hw-art {
  transform: translateY(-0.35rem);
}

/* ---- the aside: a bubble, off the rail ---- */

/* no node, and the rail is NOT interrupted — that is the whole point */
.hw-step--aside {
  padding-bottom: 2.25rem;
}

.hw-bubble {
  position: relative;
  grid-column: 3;
  display: flex;
  align-items: flex-start;
  gap: 0.875rem;
  max-width: 30rem;
  padding: 0.875rem 1.125rem;
  background-color: var(--primitive--c-white);
  border-radius: 4px;
  box-shadow: inset 0 0 0 0.75px var(--hw-ring);
  transition: transform var(--hw-dur) var(--hw-ease);
}

/* lifts on hover, but the hairline does not change - see .hw-card above */
.hw-bubble:hover {
  transform: translateY(-0.35rem);
}

/* the leader back to the rail. Its width is the glyph column plus both gaps
   plus half the rail column — change .hw-step's grid and this follows. */
.hw-bubble::before {
  content: "";
  position: absolute;
  right: 100%;
  top: 1.6rem;
  width: 8.5rem;
  border-top: 1px dashed var(--primitive--c-blue);
  opacity: 0.75;
}

/* the tail. It sits over the bubble's own hairline on purpose: the outline
   should open into the tail rather than run behind it. */
.hw-bubble::after {
  content: "";
  position: absolute;
  left: -5px;
  top: 1.25rem;
  width: 9px;
  height: 9px;
  background-color: var(--primitive--c-white);
  border-left: 0.75px solid var(--hw-ring);
  border-bottom: 0.75px solid var(--hw-ring);
  transform: rotate(45deg);
}

/* The lamp. Its glow is the pale ramp on the shared 2s LINEAR sweep — the one
   loop in the section, and the language's own recipe for a moving light:
   an oversized radial gradient panned diagonally across the host. At both ends
   of the travel the centre sits ~103% of the light's radius away, so the chip
   is flat #cfe7fc at the start and at the end and the wrap swaps two identical
   frames. */
.hw-lamp {
  position: relative;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: 8px;
  background-color: var(--hw-w2);
  background-image: radial-gradient(circle, var(--hw-w0) 0%, var(--hw-w1) 31%, var(--hw-w2) 100%);
  background-size: 260% 260%;
  background-position: 140% 118%;
  background-repeat: no-repeat;
  animation: hw-glow 2s linear infinite;
  /* pauses rather than detaches: removing the animation restarts it at peak
     brightness, so an observer near its threshold makes the light jump */
  animation-play-state: paused;
}

.hw-lamp.is-running {
  animation-play-state: running;
}

@keyframes hw-glow {
  from { background-position: 140% 118%; }
  to   { background-position: -40% -22%; }
}

.hw-lamp svg {
  position: relative;
  display: block;
  width: 1.125rem;
  height: auto;
}

.hw-lamp-ln {
  fill: none;
  stroke: var(--primitive--c-black);
  stroke-width: 1.88;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.hw-bubble-text {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.5;
  letter-spacing: 0.01em;
  color: var(--hw-ink);
}

/* ---- the copy column ---- */

.hw-step-copy {
  padding-top: 0.05rem;
}

.hw-step-copy .hw-h3 {
  font-size: 1.25rem;
  line-height: 1.3;
}



/* ============================================================================
   DIRECTIONS C AND D — the motion-spec palette

   C and D deliberately do NOT use --hw-b* / --hw-w*, the three-stop ramps at
   the top of this file. Those are a coarser reading of the same brand
   gradient; the five-stop --g-line and --g-pale below are the values the
   motion spec carries, and they are what the grid, the risers and the rail are
   drawn from. Both live here rather than in :root because nothing outside .hw
   should inherit them.
   ========================================================================== */

.hw-c,
.hw-d {
  --hw-ink2: #0b0b0c;
  --hw-muted: #5c5e62;
  --hw-faint: #8e9095;
  /* the section's own tone, not a near-miss of it: in situ .hw-c paints this
     inside .section_template-how, which is neutral-100. One step off shows up
     as a band at the seam - the exact bug NOTES.md records for this section. */
  --hw-paper: var(--primitive--c-neutral-100);
  --hw-hair: #e3e3e1;

  /* the aside floats on this, not on a ring */
  --hw-lift: 0 1px 2px rgba(0, 0, 0, 0.05), 0 12px 32px rgba(0, 0, 0, 0.06);

  --hw-g-line: linear-gradient(180deg, #fcc993 0%, #f6aebc 24%, #f094e5 49%,
                               #9f8ce9 74%, #4e84ec 100%);

  background-color: var(--hw-paper);
  color: var(--hw-ink2);
}

/* ---- type ----

   32 / 19 / 15. The section heading is no longer sized here — css/section-title.css
   owns it, at the same 2rem/500 the live site gives "Secure & Reliable by design".
   Below it the weight does the work: .hw-land-h is 19px/500 beside 15px body.
   .hw-h3 above is 18px/500 beside 12.5px body — too flat to separate — which is
   why C and D do not reuse it. */

.hw-title--mid {
  /* section-title.css caps the heading at 52ch, which leaves a centred line
     sitting inside a narrow left-hand box rather than centred on the section */
  max-width: none;
  text-align: center;
}

.hw-land-h {
  margin: 0 0 0.4375rem;
  font-size: 1.1875rem;
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.016em;
  color: var(--hw-ink2);
}

.hw-land-p {
  margin: 0;
  max-width: 34ch;
  font-size: 0.9375rem;
  line-height: 1.6;
  color: var(--hw-muted);
}

/* .text-style-caption, the lockup the live page already uses for "Trusted by
   teams who want to ship fast" - mono 12/1.3 at -0.02em, SENTENCE CASE, and no
   colour of its own so it takes the section's ink. The base .hw-cap above is a
   different mark (11/1 uppercase at +0.12em); C and D take the page's one so
   the two captions read as the same element rather than as near-misses.

   The 4px square and the 0.5rem gap in .hw-tag already match .vector._4x4 and
   its row, so only the type changes here. */
.hw-c .hw-cap,
.hw-d .hw-cap {
  font-size: 0.75rem;
  line-height: 1.3;
  letter-spacing: -0.02em;
  text-transform: none;
  color: inherit;
}

/* the same yellow .vector._4x4 carries next to the caption on the live page,
   taken from the primitive rather than restated as a hex - one source */
.hw-c .hw-tag::before,
.hw-d .hw-tag::before {
  background-color: var(--primitive--c-yellow);
}


/* ============================================================================
   C — the grid landscape
   ========================================================================== */

/* C is FULL BLEED, and the section's own gutter is handed to its children
   instead. A landscape inside a padded box shows the box: the mesh stops on
   three visible straight edges and reads as a cut rather than as ground
   running past the frame. */
.hw.hw-c {
  padding-left: 0;
  padding-right: 0;
  padding-bottom: 0;
}

.hw-title--land {
  padding-left: 3.75rem;
}

/* The svg scales proportionally, so its 1200x640 viewBox IS this box's aspect,
   and every overlay below can be positioned as a percentage of it — the
   generator emits --x and --y straight out of the same geometry that drew the
   hills, so a retuned peak moves its riser AND its copy together. */
.hw-land {
  position: relative;
}

.hw-land-svg {
  display: block;
  width: 100%;
  height: auto;
}

/* Per-quad face colour rides in on a custom property rather than a fill
   ATTRIBUTE, the same trick gen_how.py uses for --hw-ext-fill: a CSS
   declaration beats an inline attribute, and mixing the two is what cost the
   old shield a debugging cycle. */
.hw-land-face {
  fill: var(--q);
  stroke: none;
}

/* The wires are an alpha MATTE, not strokes — the source's own rig, the one
   NOTES.md describes as line art matted over a precomp holding the gradient.
   Painting the matte back to front with opaque black fills applies exactly the
   same occlusion to the wires that the body layer gets, so a near hill hides
   the ones behind it. Black and white here are mask values, not colours. */
.hw-land-wire {
  fill: #000;
  stroke: #fff;
  /* 0.85 PIXELS, and one value at every breakpoint. non-scaling-stroke pins
     the line to CSS pixels while the mesh scales with the page, so the ink to
     cell ratio runs ~1:48 at 1280 and ~1:14 at 375 — the field is a wireframe
     on a desktop and a grey tint on a phone at the same number. It was 1.1
     first and mobile was the tell.

     Do NOT drop non-scaling-stroke to "fix" that ratio: the stroke would then
     scale with the mesh and land at ~0.27px on a phone, which is the exact
     failure NOTES.md records for the hw-b plates — they read as untouched
     silhouettes. 0.85 was picked over 0.7 because it still holds at 375. */
  stroke-width: 0.85;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}

.hw-land-hide {
  fill: #000;
}

.hw-land-ink {
  fill: url(#hw-wire-ramp);
}

.hw-land-gleam-e {
  fill: url(#hw-gleam);
}

/* The gleam. LINEAR, because the spec puts every background loop on linear and
   reserves Quint for things responding to the user. 14s: the measured 2.00s
   belongs to marks a fraction of this width, and anything under ~10s here
   reads as a strobe rather than a drift. The travel overshoots the field at
   both ends so the wrap swaps two identical empty frames — the same reason
   .hw-bar::after runs -130% to 130% instead of -100% to 100%.

   Paused rather than detached, for the reason at the top of js/how.js. */
.hw-land-gleam {
  transform-box: view-box;
  transform-origin: 0 0;
  animation: hw-gleam 14s linear infinite;
  animation-play-state: paused;
}

/* Gated through .hw-land, the STATIONARY wrapper, not through the gleam itself:
   at frame 0 the group is translated a full field clear of the viewBox, so an
   IntersectionObserver watching it never fires and the light never starts. */
.hw-land.is-running .hw-land-gleam {
  animation-play-state: running;
}

@keyframes hw-gleam {
  from { transform: translate(1100px, 690px); }
  to   { transform: translate(-1520px, -960px); }
}

.hw-land-riser {
  stroke: url(#hw-line-ramp);
  stroke-width: 1.6;
  vector-effect: non-scaling-stroke;
}

.hw-land-node {
  fill: var(--hw-ink2);
}

/* TWO stems a side, one shown at a time. The generator draws a --d pair and an
   --m pair on the same feet at different tops, because y1 is an SVG attribute
   and no media query can reach it. On the desktop tops the phone's two copy
   blocks came out level and read as one wall of text; the swap in the 767 block
   below stands one stem tall and the other short so they step instead. */
.hw-land-riser--m,
.hw-land-node--m {
  display: none;
}

/* --y is the node's y as a percentage of the viewBox, so `bottom` puts the
   block's last line 1.25rem clear of the node it belongs to. */
.hw-land-copy {
  position: absolute;
  bottom: calc(100% - var(--y) + 1.25rem);
}

.hw-land-copy--l {
  left: 3.75rem;
  right: calc(100% - var(--x) + 1.5rem);
  text-align: right;
}

.hw-land-copy--r {
  left: calc(var(--x) + 1.5rem);
  right: 3.75rem;
}

/* the 4px square belongs on the outside of the lockup on both sides */
.hw-land-copy--l .hw-tag {
  flex-direction: row-reverse;
}

.hw-land-copy--l .hw-land-p {
  margin-left: auto;
}

.hw-c .hw-bubble {
  position: absolute;
  left: 50%;
  bottom: 7%;
  transform: translateX(-50%);
}

.hw-c .hw-bubble:hover {
  transform: translateX(-50%) translateY(-0.35rem);
}


/* ============================================================================
   D — the centre rail
   ========================================================================== */

.hw-mid {
  position: relative;
  max-width: 62rem;
  margin: 0 auto;
}

/* ONE rail for the whole column, drawn once. Per-segment rails restart the ramp
   at every pill, which is the same trap .hw-step::before works around by
   slicing the gradient across its segments; here there is nothing to slice
   because there is only one element. */
.hw-mid-rail {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1.6px;
  margin-left: -0.8px;
  background-image: var(--hw-g-line);
}

.hw-mid-flow {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* css/site.css ships a Webflow/Relume `li { margin: 4px 0 8px; padding-left:
   20px }`. The collapsed margin opens a hole in the row rhythm and the padding
   slides every row off centre, so this reset is load-bearing. */
.hw-mid-step {
  position: relative;
  margin: 0;
  padding: 0 0 3rem;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  justify-items: center;
}

.hw-mid-step:last-child {
  padding-bottom: 0;
}

.hw-pill {
  grid-column: 1 / -1;
  display: inline-flex;
  align-items: center;
  padding: 0.4375rem 1.125rem;
  border-radius: 100px;
  background-color: var(--hw-ink2);
  color: var(--primitive--c-white);
  font-family: var(--font--saans-mono);
  font-size: 0.625rem;
  font-weight: 500;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}

.hw-pill--quiet {
  background-color: var(--primitive--c-white);
  color: var(--hw-muted);
  box-shadow: inset 0 0 0 1px var(--hw-hair);
}

.hw-mid-copy {
  margin-top: 1.75rem;
}

.hw-mid-copy--r {
  grid-column: 2;
  justify-self: start;
  padding-left: 2rem;
}

.hw-mid-copy--l {
  grid-column: 1;
  justify-self: end;
  padding-right: 2rem;
  text-align: right;
}

.hw-mid-copy--l .hw-land-p {
  margin-left: auto;
}

/* Not a step: no pill, so the rail runs past it unbroken. The bubble sits
   ASTRIDE the rail rather than beside it — with no tail there is nothing to
   point, and straddling reads as "this is about the whole programme" instead
   of "this is about the segment on my left". */
.hw-mid-step--aside {
  padding-bottom: 3rem;
}

.hw-d .hw-bubble {
  grid-column: 1 / -1;
}


/* ============================================================================
   THE ASIDE — shared by C and D

   B's bubble is left exactly as it was: it is kept on the page for reference
   and its dashed leader is part of what is being compared against. Everything
   here is scoped to C and D instead of rewriting .hw-bubble outright.
   ========================================================================== */

/* The leader and the tail, both gone. The 8.5rem leader width was derived from
   .hw-step's grid, so that coupling goes with it. No arrow either: on a pill
   there is nothing for one to point at, and the lift alone says it floats. */
.hw-c .hw-bubble::before,
.hw-c .hw-bubble::after,
.hw-d .hw-bubble::before,
.hw-d .hw-bubble::after {
  content: none;
}

.hw-c .hw-bubble,
.hw-d .hw-bubble {
  align-items: center;
  gap: 0.6875rem;
  max-width: none;
  padding: 0.4375rem 1.5rem 0.4375rem 0.5rem;
  border-radius: 100px;
  box-shadow: inset 0 0 0 1px var(--hw-hair), var(--hw-lift);
}

.hw-c .hw-bubble-text,
.hw-d .hw-bubble-text {
  font-size: 0.875rem;
  line-height: 1.4;
  color: var(--hw-muted);
  white-space: nowrap;
}

/* The atlas's isometric cube, drawn as SVG rather than with CSS 3D: the pill is
   inline content and a preserve-3d child inside it is fragile. Its gradient id
   is scoped per copy (hw-cube-c, hw-cube-d) because both directions render on
   the same page and a duplicate id would hand one cube the other's fill. */
.hw-cube {
  display: block;
  flex: 0 0 auto;
  width: 2.25rem;
  height: 2.25rem;
  overflow: visible;
}

.hw-cube-top {
  fill: var(--hw-cube-fill);
  stroke: #b48cd8;
  stroke-width: 1.41;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
}

.hw-cube-side {
  fill: none;
  stroke: #9fb6de;
  stroke-width: 1.41;
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
  stroke-dasharray: 3 2.6;
}


/* ============================================================================
   BREAKPOINTS — these must stay last. Rules appended after them would
   silently win over the overrides.
   ========================================================================== */

@media screen and (max-width: 991px) {
  .hw {
    padding: 3.5rem 2rem;
  }

  /* C hands out the section gutter itself, so it has to track .hw's steps */
  .hw-title--land {
    padding-left: 2rem;
  }

  .hw-land-copy--l {
    left: 2rem;
  }

  .hw-land-copy--r {
    right: 2rem;
  }
}

@media screen and (max-width: 767px) {
  .hw {
    padding: 3rem 1.25rem;
  }

  /* A collapses to one column: the axis keeps its full 12-month scale above a
     stacked pair of cards, so the timeline still reads. */
  .hw-cards {
    grid-template-columns: minmax(0, 1fr);
  }

  .hw-drop::before {
    display: none;
  }

  .hw-step {
    grid-template-columns: 1rem 3.5rem minmax(0, 1fr);
    gap: 0 0.875rem;
  }

  /* the leader tracks the grid above: 3.5rem glyph + 2 x 0.875rem gap
     + half the 1rem rail column */
  .hw-bubble::before {
    width: 5.75rem;
  }

  .hw-num {
    font-size: 2rem;
  }

  /* CROP, don't compress. Scaling the whole field down uniformly took the cells
     from 53px to 16px and the mesh stopped reading as a wireframe. The svg is
     emitted with preserveAspectRatio="slice", so pinning a height makes it
     cover the box and trim the sides instead: the cells keep a usable size and
     fewer columns are shown.

     26rem IS A FLOOR, NOT A LOOK. slice scales by max(w/1200, h/640) and only
     ever crops the axis that loses, so the height has to keep h/640 winning
     across the WHOLE range or the crop silently flips axis. At 767 the width
     term is 767/1200 = 0.639, so anything under 25.6rem starts trimming the sky
     and the foreground instead of the sides — which is what 22rem did between
     767 and ~660. 26rem holds the scale at a constant 0.65 from 767 down to the
     narrowest phone: cells stay 32.5px everywhere and narrow screens simply see
     fewer columns.

     Because the scale is then always h/640, the vertical mapping stays exact —
     which is what lets --y below keep working as a plain percentage. */
  .hw-land-svg {
    height: 26rem;
  }

  /* NOTHING DROPS BELOW THE FIELD. The risers stay, and the copy stays on the
     grid — it just moves from beside its riser to above it. Beside is not an
     option here: with the field cropped, the two risers land about 242px apart
     on a 375px screen, which leaves ~44px outboard of the left one. Each block
     takes the half it stands in, so the two sit side by side and close
     together, each still over its own riser. */
  .hw-land-riser--d,
  .hw-land-node--d {
    display: none;
  }

  /* inline is the initial display for an svg shape, so this is a plain undo of
     the base rule rather than a layout mode being imposed on it */
  .hw-land-riser--m,
  .hw-land-node--m {
    display: inline;
  }

  .hw-land-copy {
    left: 1.25rem;
    right: 1.25rem;
    bottom: calc(100% - var(--ym) + 1.25rem);
  }

  .hw-land-copy--l {
    right: calc(50% + 0.5rem);
    text-align: left;
  }

  .hw-land-copy--r {
    left: calc(50% + 0.5rem);
  }

  .hw-land-h {
    font-size: 1rem;
    line-height: 1.25;
  }

  .hw-land-copy .hw-land-p {
    font-size: 0.8125rem;
  }

  .hw-title--land {
    padding-left: 1.25rem;
  }

  .hw-land-copy--l .hw-tag {
    flex-direction: row;
  }

  .hw-land-copy--l .hw-land-p,
  .hw-mid-copy--l .hw-land-p {
    margin-left: 0;
  }

  /* The aside stays on the field too, over the flat foreground - but it SIZES
     TO ITS SENTENCE. left+right pinned it to both gutters at every width, so at
     622 it ran 582px wide around 396px of content and read as a bar rather than
     as a pill. left+right stay as the OUTER BOUND and width: fit-content does
     the sizing inside them.

     margin-inline, not the desktop's left: 50% + translateX(-50%): a shrink to
     fit box offset to the middle only gets the half of the container to its
     right as available width, and the pill collapsed to 187.5px on a 375 screen.
     Auto margins centre it with the whole box to size against.

     white-space: normal below lets it wrap on a narrow phone, where the sentence
     genuinely needs the full 335px. text-wrap: balance was tried there and is
     wrong: the box is still max-width, so balancing just empties its right half. */
  .hw-c .hw-bubble {
    left: 1.25rem;
    right: 1.25rem;
    bottom: 5%;
    transform: none;
    width: fit-content;
    margin-inline: auto;
    max-width: 100%;
  }

  .hw-c .hw-bubble:hover {
    transform: translateY(-0.35rem);
  }

  .hw-c .hw-bubble-text,
  .hw-d .hw-bubble-text {
    white-space: normal;
  }

  /* D collapses to a single left-hand rail with both steps on the same side */
  .hw-title--mid {
    text-align: left;
  }

  .hw-mid-rail {
    left: 0.5rem;
  }

  .hw-mid-step {
    grid-template-columns: minmax(0, 1fr);
    justify-items: start;
    padding-left: 2rem;
  }

  .hw-mid-copy--r,
  .hw-mid-copy--l,
  .hw-d .hw-bubble {
    grid-column: 1;
    justify-self: start;
    padding-left: 0;
    padding-right: 0;
    text-align: left;
  }

  .hw-d .hw-bubble {
    padding: 0.4375rem 1.5rem 0.4375rem 0.5rem;
  }
}

@media screen and (max-width: 479px) {
  /* section-title.css centres every section heading at this width. C's heading
     carries the section's left gutter on its own (.hw.hw-c zeroes the section's
     side padding), and a one-sided gutter would push that centred line 10px off
     the page's centre line. Match it on the right and the centring is true. */
  .hw-title--land {
    padding-right: 1.25rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* resting values are already the correct static frame, so switching motion
     off changes nothing about how either direction reads */
  .hw-bar::after {
    animation: none;
  }

  /* belt and braces: js/how.js already refuses to arm the wipe under reduced
     motion, so the clip never applies — this makes it impossible either way */
  .hw-runway.is-armed .hw-bar,
  .hw-runway.is-armed.is-revealed .hw-bar {
    clip-path: none;
    transition: none;
  }

  .hw-lamp,
  .hw-land-gleam {
    animation: none;
  }

  .hw-art,
  .hw-bubble,
  .hw-card {
    transition: none;
  }
}
