/* ===== Easy-to-edit variables ===== */
:root {
  --gold: #c9a54a;
  --gold-dark: #a9822f;
  --dark: #141414;
  --white: #ffffff;
  --font: "Helvetica Neue", Arial, sans-serif;

  /*
   * ===== HOW FAR THE WORDMARK SITS UNDER THE LOGO =====
   *
   * ONE NUMBER, USED BY EVERY PAGE THAT SHOWS THE MARK. It was three separate
   * numbers — the hero at -6px, join.html and the blog at -26px — so the gap
   * was 24px on the home page and 4px everywhere else. Nothing was wrong with
   * any single value; they had simply been tuned one page at a time, and three
   * numbers meant to be equal with nothing keeping them so always drift.
   *
   * THE HOME PAGE IS THE REFERENCE, at the owner's instruction, so this is the
   * hero's value and the hero reads it from here too. A page cannot now be
   * adjusted on its own without moving all of them, which is the point.
   *
   * ===== WHY IT IS NEGATIVE AT ALL =====
   *
   * The GIF carries its own empty space. Measured from the file's alpha channel
   * across all 45 frames: artwork occupies y 96..382 of a 480px canvas, so 97px
   * below it (20.2%) is transparent. Rendered 150px tall that is 30.3px of
   * nothing between the diamond and the bottom of the element.
   *
   * So the visible gap is 30.3px MINUS this value — about 24px today. Change
   * the logo's width and this needs recomputing in the same proportion, because
   * the dead space scales with it.
   *
   * THE REAL FIX IS TO CROP THE GIF, after which this becomes 0 and every use
   * of the mark stops carrying 40% dead height. Not done here: re-exporting an
   * animated GIF is a change to the owner's artwork, not a layout task.
   */
  --brand-pull: -6px;

  /*
   * ===== HOW FAR THE BRAND BLOCK SITS BELOW THE NAV BAR =====
   *
   * MEASURED IN CHROME. All three pages carry the same 50.4px nav; what differed
   * was the gap under it:
   *
   *   index.html   6px   (.hero-content's own padding)   logo top 56.4
   *   join.html   24px   (.join-wrap's padding)          logo top 74.4
   *   blog.html   24px   (the masthead's margin)         logo top 74.4
   *
   * So the home page's logo sat 18px higher than the other two — not because
   * anything was mis-set, but because each page had been spaced on its own.
   *
   * THE HOME PAGE IS THE REFERENCE, at the owner's instruction, so 6px it is.
   * Home is tight by necessity: it is built to fit one viewport without
   * scrolling, and that 6px is what buys the room.
   */
  --brand-top: 6px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

html, body {
  height: 100%;
  overflow: hidden; /* whole page fits in one viewport, no scrolling */
}

/*
 * ===== RESERVE THE SCROLLBAR'S WIDTH WHETHER OR NOT IT IS SHOWING =====
 *
 * MEASURED IN CHROME, at 721px wide:
 *
 *   index.html   scrollbar 15px   content 706px   logo centre 353.0
 *   join.html    scrollbar 15px   content 706px   logo centre 353.0
 *   blog.html    scrollbar  0px   content 721px   logo centre 360.5
 *
 * Every page centred its logo perfectly — offset 0.0 on all three. They simply
 * were not centring in the same box. The blog has no posts published yet, so it
 * is short enough not to scroll, so it has no scrollbar, so its viewport is 15px
 * wider and everything on it sits 7.5px right of where it sits everywhere else.
 *
 * NOTHING IN THE MASTHEAD WAS WRONG. Three rounds were spent on --brand-pull and
 * on margins, and the answer was that one page was not showing a scrollbar. A
 * layout question that no reading of the CSS can answer, because the CSS is the
 * same on all three — only the content height differs.
 *
 * `scrollbar-gutter: stable` always reserves the space, so the centre line does
 * not move when a page happens to be short. It also stops the horizontal jump
 * you would otherwise get on the blog the moment the first post is published and
 * the page grows past one screen.
 *
 * ON html, NOT body: the viewport's scrollbar belongs to the root element.
 */
html {
  scrollbar-gutter: stable;
}

body {
  font-family: var(--font);
  color: var(--dark);
  /* Pattern sits on the page background so it shows through in the empty
     space below the Contact section, all the way to the footer. Done purely
     with background paint (no extra boxes, no positioning, no heights), so
     it cannot affect the flex layout — the About section below keeps an
     explicit white background so it looks exactly as before. */
  /* Tint only — the damask itself now lives on the drifting layers below
     (body::before / ::after) so it can move as one continuous backdrop behind
     the entire page rather than stopping at the hero. */
  background-image:
    linear-gradient(180deg, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0) 18%, rgba(255,255,255,0) 70%, rgba(20,16,8,0.18) 100%),
    radial-gradient(ellipse at center, rgba(255,255,255,0) 0%, rgba(120,95,40,0.10) 100%);
  background-repeat: no-repeat, no-repeat;
  background-size: cover, cover;
  background-position: center, center;
  background-attachment: fixed, fixed;
  line-height: 1.4;
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* ===== Living backdrop: two-layer parallax drift, whole page =====
 *
 * Two copies of the same damask at different scales, drifting on different
 * axes at different speeds. The mismatch between them is what the eye reads as
 * depth — neither layer is interesting on its own, which is the point.
 *
 * FIXED, NOT ABSOLUTE: pinned to the viewport and sitting behind everything at
 * z-index -2, so one continuous backdrop runs from the nav down to the footer
 * instead of stopping at the hero. Sections that want to sit on top of it just
 * paint their own background (About is white); sections that want to show it
 * stay transparent (Contact).
 *
 * WHY TRANSFORMS, NOT background-position: transforms are handled by the
 * compositor, so the browser never repaints the pattern — it moves a layer it
 * has already drawn. Animating background-position would repaint a
 * full-viewport image every frame, which is how effects like this normally end
 * up cooking a laptop fan.
 *
 * WHY 200% AND OFFSET BY -50%: each layer is four times the area it needs, so
 * drifting can never expose an edge.
 *
 * SEAMLESS LOOP: each animation travels EXACTLY one tile — 380px across for
 * the near layer, one tile height (560 x 1228/756 = 909.6px) down for the far
 * one. Landing precisely on the next tile makes the reset invisible. Change a
 * background-size and the matching keyframe must be recomputed, or the loop
 * will visibly jump.
 */
body::before,
body::after {
  content: "";
  position: fixed;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  z-index: -2;
  background-image: url("assets/backdrop.jpg");
  background-repeat: repeat;
  will-change: transform;
  pointer-events: none;
}

/* Near layer: original scale, drifting sideways. ~6.9px/s. */
body::before {
  background-size: 380px auto;
  animation: drift-near 55s linear infinite;
}

/* Far layer: larger tile, part-transparent, drifting downward instead. ~9.6px/s. */
body::after {
  background-size: 560px auto;
  opacity: 0.38;
  animation: drift-far 95s linear infinite;
}

@keyframes drift-near {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-380px, 0, 0); }
}

@keyframes drift-far {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -909.6px, 0); }
}

/* Motion is decoration, so it goes away for anyone whose system asks for less
   of it. The backdrop still renders; it simply holds still. */
@media (prefers-reduced-motion: reduce) {
  body::before,
  body::after {
    animation: none;
  }
}

/* ===== Hero / Backdrop section ===== */
/* ===== THE HERO TAKES THE HEIGHT IT NEEDS, AND NO MORE =====
 *
 * This was `flex: 0 0 40vh` — a fixed share of the viewport. Its contents are
 * clamp()ed to a near-fixed height, so the difference between the two was dead
 * space that grew about 0.4px for every pixel of viewport height, and
 * justify-content:flex-start dumped all of it between the button and the
 * countdown band below. Measured: 14px of gap at a 919px viewport, 178px at
 * 1330, 286px at 1600.
 *
 * THE FIRST FIX WAS WRONG AND MADE IT WORSE. Pinning the button to the hero's
 * base with margin-top:auto moved that space ABOVE the button instead of
 * below. The gap was still there, still growing — it had simply changed sides,
 * and it moved a button nobody asked to move.
 *
 * Sizing to content deletes the space rather than relocating it. There is now
 * no slack anywhere inside the hero at any height, so the button sits its
 * stated 15px under the tagline and the countdown sits its stated margin under
 * the button, on every screen.
 *
 * WHERE THE SPARE HEIGHT GOES INSTEAD: .content-sections is flex:1, so it
 * absorbs everything left over — below the last section, where empty space is
 * simply the bottom of the page rather than a hole between two elements.
 */
.hero {
  position: relative;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Tiled instead of stretched: the source file is only 756x1228px, so
   object-fit:cover was upscaling it 3-7x on large/fullscreen displays,
   which is what caused the blur/pixelation. It's a seamless repeating
   pattern, so tiling at (near) native resolution keeps it sharp at any
   window size instead of stretching a single copy to fill the viewport. */
/* Intentionally empty of pattern. The damask is now the page-wide drifting
   backdrop (body::before / ::after), which shows straight through here — so
   the hero and the Contact section share one continuous, moving surface
   instead of three separate copies that had to be kept aligned. Kept as an
   element because .hero-overlay's tint is layered against it. */
.hero-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ===== Nav bar ===== */
.navbar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: flex-end; /* logo removed, links stay right-aligned; the
                                ticker, when present, claims the space to their
                                left via flex: 1 */
  gap: 0;
  padding: 14px 5%;
  flex: 0 0 auto;
}

/* ===== Job-market ticker =====
 *
 * Shares the navbar line with the links: the ticker takes the space the links
 * do not, which is why .navbar switches from flex-end to space-between and the
 * links get margin-left:auto as a fallback for when the ticker is hidden.
 *
 * TRANSLATE, NOT background-position OR left. A transform is composited, so
 * the strip never repaints while it moves — the same reason the drifting
 * damask uses one. Animating `left` here would repaint the navbar sixty times
 * a second for the whole time somebody has the page open.
 *
 * The track holds TWO copies of the list and travels exactly -50%, so the
 * moment it resets, copy two is sitting where copy one began and the seam is
 * invisible. One copy would snap back to a gap.
 */
.nav-ticker {
  flex: 1 1 auto;
  min-width: 0;          /* lets it shrink inside the flex row instead of pushing the links off */
  overflow: hidden;
  margin-right: 28px;
  border-radius: 2px;
  /*
   * GOLD, NOT BRONZE.
   *
   * The first pass was too faint, so the second darkened it — and darkening a
   * gold moves it toward brown, which is exactly how you get bronze. What was
   * actually missing was CONTRAST, and contrast on a metal comes from a
   * defined edge and a highlight, not from a heavier fill.
   *
   * So: the fill goes back up toward the palette's own --gold (#c9a54a), and
   * the four stops describe a surface rather than a colour — pale at the top
   * where light lands, a brighter sheen just above the middle, deeper toward
   * the bottom edge. It is the sheen that reads as metal; without it any
   * flat gold looks like paint.
   */
  background: linear-gradient(
    180deg,
    rgba(226, 205, 148, 0.34) 0%,
    rgba(201, 165, 74, 0.30) 44%,
    rgba(238, 224, 180, 0.26) 52%,
    rgba(169, 130, 47, 0.30) 100%
  );
  /*
   * A bright hairline on top and a darker one beneath. This pair is doing most
   * of the work: it is what separates the strip from the damask without
   * needing a heavy fill to do it.
   */
  box-shadow:
    inset 0 1px 0 rgba(255, 250, 235, 0.55),
    inset 0 -1px 0 rgba(150, 113, 34, 0.40);
  /* A soft edge at both ends, so items arrive and leave rather than being
     chopped off at a hard boundary. */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 6%, #000 94%, transparent);
}

.nav-ticker[hidden] { display: none; }

.nav-ticker-track {
  display: flex;
  width: max-content;
  /*
   * The duration here is only a fallback. ticker.js overwrites it from the
   * measured width so the strip keeps a constant PACE — a fixed duration would
   * mean five roles crawl and twenty-five race, purely because the track got
   * longer. See the note beside `travel` in ticker.js.
   */
  animation: nav-ticker-scroll 40s linear infinite;
}
@keyframes nav-ticker-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* Stops while somebody is actually reading it. */
.nav-ticker:hover .nav-ticker-track { animation-play-state: paused; }

/*
 * MOTION IS NOT OPTIONAL FOR EVERYONE. Continuous horizontal movement is a
 * known trigger for vestibular disorders, and a ticker is the worst offender
 * because it never stops. Anyone who has asked their system for less motion
 * gets a static strip with the first few items, which still carries the
 * information.
 */
@media (prefers-reduced-motion: reduce) {
  .nav-ticker-track { animation: none; }
}

.nav-ticker-item {
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  padding: 4px 20px;
  white-space: nowrap;
  font-size: 11px;
  letter-spacing: 0.3px;
  /*
   * NO FILL OF ITS OWN. A second wash on top of the band was the other half of
   * the bronze: two translucent golds stacked make mud, and the cell edges
   * then had to be darkened further to stay visible, which made it worse. A
   * single hairline divider separates the items and lets the band stay clean.
   */
  border-right: 1px solid rgba(169, 130, 47, 0.32);
}

/*
 * Impossible to mistake for the real thing — see DEMO in ticker.js — without
 * being a heavy block that unbalances the strip. Outlined rather than filled:
 * it reads as a label ABOUT the ticker rather than as another reading in it.
 */
.nav-ticker-item.is-sample {
  color: #6d5316;
  font-weight: 700;
  font-size: 9px;
  letter-spacing: 1.4px;
  background: rgba(255, 252, 244, 0.42);
}

/*
 * The contrast lives here now, not in the fill. --ink rather than near-black:
 * on a warm gold, a cold near-black reads as printed-on, while the palette's
 * own brown-grey sits in the same family and still carries at 11px.
 */
.nav-ticker-role {
  color: var(--ink);
  font-weight: 700;
}

/* Muted from the flat green/red a trading screen would use — this is a
   marketing header, and saturated signal colours on gold look cheap. */
.nav-ticker-move { color: #7a5c1c; font-weight: 700; }
.nav-ticker-move.is-up { color: #2c6b48; }
.nav-ticker-move.is-down { color: #9c5049; }

/* Smallest and lightest of the three: the region is context, not the point. */
.nav-ticker-where {
  color: #6f6656;
  font-size: 9.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.9px;
}

.nav-links {
  list-style: none;
  display: flex;
  gap: 32px;
  /* Keeps the links right-aligned when the ticker is hidden and there is
     nothing else in the row to push them over. */
  margin-left: auto;
  flex: 0 0 auto;
}

.nav-links a {
  color: var(--dark);
  text-decoration: none;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  opacity: 0.8;
  transition: opacity 0.2s ease, color 0.2s ease;
}

.nav-links a:hover { opacity: 1; color: var(--gold-dark); }

/* ===== Hero content ===== */
.hero-content {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start; /* shifted up instead of vertically centered */
  text-align: center;
  /* The 6px is --brand-top, read from the variable rather than left as a
     literal — the reference page must not become the one page disagreeing with
     the value it defines. */
  padding: var(--brand-top) 20px 4px;
  min-height: 0;
  overflow: hidden;
}

/* ===== Logo "come to life" treatment =====
   The shimmer (breathing glow + outline glow + diagonal gleam sweep) is
   baked into assets/logo-shimmer-transparent.gif, generated from the same
   logo artwork. See make_shimmer_gif.py if you want to regenerate it
   (e.g. to change speed, glow color, or gleam angle). */
.hero-logo-wrap {
  position: relative;
  display: inline-block;
  /* The shared value, which is this page's own — see --brand-pull. Read from
     the variable rather than left as a literal, so the home page cannot quietly
     become the one page that disagrees with the reference it defines. */
  margin-bottom: var(--brand-pull);
}

.hero-logo {
  position: relative;
  z-index: 1;
  width: 150px; /* source GIF is rendered at 480px, so this stays crisp */
  height: auto;
  display: block;
}

.hero-content h1 {
  color: var(--dark);
  font-size: clamp(18px, 3vw, 32px);
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  margin-bottom: 4px;
}

.rotate-word {
  display: inline-block;
  color: var(--gold-dark);
  font-weight: 700;
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.rotate-word.rotate-out {
  opacity: 0;
  transform: translateY(-6px);
}

.hero-content p {
  color: #3a3226;
  font-size: clamp(13px, 1.4vw, 16px);
  max-width: 600px;
  margin-top: 20px;
  margin-bottom: 20px; /* breathing room before the CTA button */
  opacity: 0.9;
}

.cta-button {
  display: inline-block;
  margin-top: 15px;
  padding: 8px 28px;
  background: linear-gradient(135deg, var(--gold), var(--gold-dark));
  color: var(--white);
  text-decoration: none;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  border-radius: 4px;
  transition: transform 0.2s ease;
}

.cta-button:hover {
  transform: translateY(-2px);
}

/* ===== Content sections below the fold =====
   Previously each section was flex:1, individually centering its own
   (short) content inside a full equal share of the leftover space — that
   left big dead gaps both above and below the content in both sections.
   Now the wrapper takes the leftover space and centers the two sections
   as one compact group, while each section only takes the height its
   actual content needs — one intentional gap instead of four scattered
   ones. */
.content-sections {
  flex: 1;
  display: flex;
  flex-direction: column;
  /*
   * ===== TOP-ALIGNED, NOT CENTRED =====
   *
   * This was `justify-content: center`, which left 94px of slack and split it
   * evenly — 47px above the countdown and 47px below the last section. Nobody
   * chose 47px; it was a remainder, and it is what made the gap under the call
   * to action look accidental, because it was.
   *
   * Worse, it moved. Every time content was added or removed anywhere in this
   * column, the remainder changed and everything shifted by half of it. That
   * is what dragged the film into the footer when the "Sign-up for Free Trial"
   * heading came out.
   *
   * Aligning to the top and stating the gap makes it a decision: 14px of
   * padding puts the countdown ~23px under the button, and it stays there
   * whatever else changes below.
   *
   * The two paint-only offsets on the film and the note were re-solved against
   * the positions this produces — columns end at 635, the footer starts at
   * 894, and that 259px holds the 153px film, the 19px note and three even
   * 29px gaps. Move this padding and those two numbers are wrong again.
   */
  /*
   * ===== SPACE-EVENLY. THE SIXTH RE-SOLVE IS WHY THIS CHANGED =====
   *
   * Everything above is the argument for flex-start, and it was right about
   * `center` — splitting a remainder in two moves everything by half of any
   * change made anywhere. But flex-start has the opposite failure and this page
   * had it badly: ALL the spare height goes to the bottom, in one lump, below
   * the last thing. On a tall window that is ~150px of dead space under the
   * resource rail with the headline, columns, film and rail packed hard against
   * each other above it. The page was not short of room. The room was all in
   * one place, and it was the one place nothing needed it.
   *
   * space-evenly hands that same spare height to the gaps BETWEEN the sections
   * instead of dumping it after them. It is not `center`: when there is no
   * slack — a short window — every gap goes to zero and this degrades to
   * exactly flex-start, so the short-window overflow that flex-start was
   * chosen to prevent still cannot happen.
   *
   * ===== AND THE TWO OFFSETS BELOW IT ARE GONE =====
   *
   * `padding-top: 14px` and `margin-top: -14px` cancelled to nothing while
   * pretending to be two decisions. The margin was the last survivor of the
   * countdown band: -30 tucked the column under the band, then -14 after the
   * band was removed. Both were measured against neighbours that have since
   * moved, which is the failure this file records five times.
   */
  justify-content: space-evenly;
  min-height: 0;
  overflow: hidden;
}

.section {
  flex: 0 0 auto;
  padding: 28px 8%;
  max-width: 900px;
  margin: 0 auto;
  width: 100%;
  /* Same centering approach as .hero-content: a flex column with
     align-items:center, so each child is centered as its own box rather
     than relying on text-align across a full-width block — that's what
     was drifting off-center at wide/maximized viewports. */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.section h2 {
  font-size: clamp(18px, 2.4vw, 26px);
  margin-bottom: 8px;
  color: var(--dark);
}

/* Extra breathing room before the icon row specifically (scoped to #about
   so the Contact section's heading spacing is untouched). Font-size is
   also capped lower than the generic .section h2 — this heading is long
   ("Your Career is a Campaign. Not a Job Search.", 43 characters), and at
   the generic 26px max a heading this length stops fitting the 900px-wide
   section on large screens and wraps onto a second line. 22px keeps it on
   one line at any viewport width.

   WRAPPING IS NOT JUST UGLY HERE. A second line adds ~27px of real layout
   box, which moves everything below it — and the film and the note under it
   are positioned by fixed-pixel transforms solved against these exact
   positions. One wrapped heading pushes the note into the footer. */
#about h2,
#contact h2 {
  font-size: clamp(16px, 2.1vw, 22px);
}

/*
 * ===== THE -70px PULL IS GONE, WITH THE BAND IT WAS SOLVED AGAINST =====
 *
 * This read -100px, then -70px, each one hand-measured against the countdown
 * band that used to sit between the hero and this heading. The band was
 * removed at the client's request and the -70 went straight on pulling: the
 * heading landed 28px INSIDE the hero's box, where the hero paints over it,
 * so "Your Career is a Campaign. Not a Job Search." simply vanished. The page
 * still rendered. Nothing failed. The headline was just not there.
 *
 * Fifth fixed offset in this file to break when its neighbour moved, and the
 * same lesson each time: a pixel solved against something else is a promise
 * that the something else will never change.
 *
 * MEASURED IN THE BROWSER, not calculated. With every negative offset removed
 * the page ran 33px past the footer — the band's space was already spoken for,
 * so removing it did not hand back a surplus. The gaps below are what four
 * measured trials landed on:
 *
 *     button to heading   24px
 *     heading to columns  10px
 *     columns to film     12px
 *     film to rail        14px
 *     rail to footer      17px
 *
 * Even, nothing cramped, nothing clipped, and no element grew — because there
 * was no room to grow one. See .content-sections for the remaining -14px.
 */
/*
 * ===== THE GAPS ARE MINIMUMS. THE BROWSER SETS THE REST =====
 *
 * These were briefly clamp(floor, Nvh, ceiling) so a tall window would breathe.
 * That overflowed the footer by 5px, and the reason is worth keeping: a vh term
 * buys height UNCONDITIONALLY. It grows the content whether or not there is
 * room for it, so on the very window where the extra space runs out it is still
 * demanding more. Two mechanisms competing for the same pixels.
 *
 * So every number below is a floor and nothing else. All the breathing is done
 * by space-evenly on .content-sections and on the two sections themselves,
 * which can only ever hand out space that already exists — when there is none,
 * every gap collapses to exactly these values and the page renders as measured.
 */
#about h2 {
  margin-top: 0;
  margin-bottom: 10px;
}

/* Keeps the About section white now that the page background carries the
   pattern. The plain background alone only paints this box (900px wide, and
   and formerly not the h2, which a negative margin-top used to pull above the
   box — that pull is gone now, so the heading sits inside it), so a single
   box-shadow floods white outward: 2000px spread grows the paint area in
   every direction, then the -2000px vertical offset drops its bottom edge
   back to exactly this box's bottom, so it fills upward and full-width but
   never bleeds down into the Contact section. .content-sections' existing
   overflow:hidden clips it at the top and sides. Box-shadow is paint-only
   and never affects layout, so the About section cannot move. */
/* Transparent, so the drifting backdrop runs unbroken from the nav to the
   footer. This previously painted white — plus a 2000px box-shadow flood to
   cover the full width and the negative-margin heading — because the pattern
   was a static page background it needed to hide. Now that the backdrop is the
   feature, there is nothing to cover.
   NOTE: the column icons must stay transparent PNGs (assets/*-icon-alpha.png)
   or they reappear as white rectangles here. */

/* Matches the CTA button's link target (#contact) */
.access-link {
  color: var(--gold-dark);
  text-decoration: underline;
}

.access-link:hover {
  color: var(--gold);
}

.section p {
  font-size: clamp(13px, 1.4vw, 16px);
}

/* ===== Video upload placeholder (Contact section) ===== */
.video-placeholder {
  position: relative;
  width: 100%;
  max-width: min(206px, 30vw); /* was 113px painted at 2.4x — see below */
  aspect-ratio: 16 / 9;
  margin-top: 16px;
  /*
   * VISUAL-ONLY SHIFT AND RESIZE. Everything about this group's position is
   * done with transforms rather than margins or width, because a real size
   * here would grow the Contact section, re-centre the group and push the
   * About section above it. Transforms paint; they do not reflow.
   *
   * THE ARITHMETIC, measured in the browser rather than guessed. The layout
   * box is 113 x 63.6px and scaling happens about its centre, so with
   * translateY(Y) and scale(S) the visible edges land at:
   *
   *     top    = centre + Y - (S/2 x 63.6)
   *     bottom = centre + Y + (S/2 x 63.6)
   *
   * Was translateY(50px) scale(2): 127px tall, with a 42px gap to the heading
   * above and 38px to the notes below. Now scale(2.4) — 20% bigger, 152px —
   * and translateY(42px), which lands both gaps at 22px. The box grows into
   * the space the gaps give up, so the group ends up shorter overall rather
   * than taller.
   */
  /*
   * 42px -> 2px. RECALCULATED, NOT NUDGED, after "Sign-up for Free Trial" was
   * removed from this section.
   *
   * That heading was ~35px of real layout box. Deleting it moved every box
   * below it, but this transform is a FIXED PIXEL OFFSET — it paints, it does
   * not reflow — so it went on shifting the film down by the same 42px from a
   * starting point that had moved. The film overflowed its own section by
   * 39px and shoved the note beneath it under the footer, where the page's
   * `overflow: hidden` cut it in half.
   *
   * THE ARITHMETIC, from positions measured in the browser rather than
   * guessed. The layout box is 113 x 63.6px and scaling is about its centre:
   *
   *     painted top = layout centre + Y - (S/2 x 63.6)
   *
   * Measured layout centre 759px, painted height 153px. The columns above end
   * at 666 and the footer starts at 894 — 228px, which holds the 153px film,
   * the 19px note and three even 19px gaps exactly. Solving for a film top of
   * 685 gives Y = 2.
   *
   * Change the scale and this number is wrong again: the two are one
   * measurement, not two decisions.
   */
  /*
   * ===== THE TRANSFORM IS GONE, AND SO IS THE ARITHMETIC ABOVE =====
   *
   * Everything from "VISUAL-ONLY SHIFT AND RESIZE" down to here described a
   * 113px box painted at 2.4x with a hand-solved pixel offset. It is kept as a
   * record because it explains why five separate comments in this file say
   * "re-solved": each was a fixed offset measured from a neighbour that later
   * moved, so every edit anywhere above silently invalidated it, and the
   * symptom was the film overflowing its section and shoving the note under
   * the footer where overflow:hidden cut it in half.
   *
   * THE REASON FOR THE HACK NO LONGER EXISTS. It was written when
   * .content-sections used justify-content:center, where growing this box
   * re-centred the whole column and pushed the About section. That container
   * is now top-aligned and flex:1, so it absorbs spare height at the BOTTOM.
   * A real size here therefore moves nothing above it, and the browser does
   * the arithmetic that was being done by hand.
   *
   * So the film is now simply the size it looks. Sized in vh as well as px so
   * it gives way on a short window rather than pushing the rail below it into
   * the footer.
   */
  max-height: 16vh;
  /*
   * A dashed border said "drop a file here", which was right when this was an
   * upload target and reads as unfinished now that it frames a film. One line
   * to put back if you preferred it: border: 2px dashed #c9c2b0.
   */
  border: 1px solid rgba(169, 130, 47, 0.35);
  border-radius: 8px;
  overflow: hidden;
  background: #fdfcfa;
}

/* The upload picker's rules lived here. It let a visitor choose a local file
   to preview — scaffolding from before there was a film to embed, and dead
   the moment there was one. */

.video-preview {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: #000;
}

/* ===== The play button =====
 *
 * OURS, NOT CHROME'S. The player is 113px wide in layout and only looks 271px
 * because of the scale above — and Chrome lays its control bar out against the
 * PRE-transform width. At 113px it collapses the whole bar to a single
 * overflow dot: no play button at all, on a film whose entire job is to be
 * clicked.
 *
 * The obvious fix — a real 271px width — was tried and cost more than it was
 * worth. Real width means real layout height, which pushed the About section
 * up past .content-sections' overflow boundary and took the "3 Impact Tools"
 * headline off the page entirely. Drawing our own button keeps the layout
 * exactly as it was and reads better than a default grey bar.
 *
 * Native controls are switched ON at the moment of first play, by video.js.
 * By then the film is running and a cramped scrub bar is a small annoyance;
 * before then, it was the difference between a film that plays and one that
 * cannot be started.
 */
.video-play {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  padding: 0;
  cursor: pointer;
  z-index: 3;
  /* Just enough veil to lift the button off a bright poster frame. */
  background: rgba(20, 16, 8, 0.28);
  transition: background 0.18s ease;
}

.video-play:hover { background: rgba(20, 16, 8, 0.14); }

/* Sized in the layout's own units, so the 2.4x scale enlarges it with
   everything else and it stays in proportion at any scale. */
.video-play-disc {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
  transition: transform 0.18s ease;
}

.video-play:hover .video-play-disc { transform: scale(1.08); }

/* A triangle from borders — no SVG to scale badly, no glyph to go missing. */
.video-play-disc::after {
  content: "";
  width: 0;
  height: 0;
  margin-left: 2.5px; /* optical centring: a triangle's mass sits left */
  border-style: solid;
  border-width: 6px 0 6px 10px;
  border-color: transparent transparent transparent #141008;
}

.video-play[hidden] { display: none; }

/* .video-preview:not(.has-video) used to hide the element until a file was
   chosen. With the film embedded the class is always present, so that rule
   could only ever hide a video that was supposed to be showing. */

/* .video-placeholder is visually enlarged/shifted via transform (doesn't
   affect layout — see its own comment), so normal-flow siblings don't
   reflow around the visual size. This clears that gap visually via
   transform (not margin-top), so it can't grow the Contact section's
   actual layout height and re-center the group, which would push the
   About section (unlike a real margin-top would). */
/*
 * ===== .faq-note WAS DELETED WITH THE LINER =====
 *
 * The rule styled one paragraph on the homepage — "Enquire with Ascent the
 * chatbot or input our Workplace Culture e-Survey." — which was removed to
 * make room for the resource rail. A grep for the class across every .html
 * and .js in this directory returns nothing, so the rule had no remaining
 * user.
 *
 * I nearly kept it "in case another page uses it" and wrote that as the
 * justification. It was not true. Dead CSS with a confident comment attached
 * is worse than dead CSS, because the next person reads the comment instead
 * of running the grep. It carried a translateY(85px) solved against the
 * homepage film's painted bottom edge, which no longer exists — so reviving
 * it on another page would have shoved that page's text 85px down for reasons
 * nobody could have reconstructed.
 */


/* Three evenly-spaced columns, no borders/background — purely a text
   layout, not visible "cards" or dividers. Each icon image was pre-
   normalized in Python to fill the same proportion of its own canvas
   (see assets/*-final.png), so a single plain size here is enough to make
   all three read as visually equal — no more per-icon size overrides. */
.about-columns {
  display: flex;
  gap: 28px;
  width: 100%;
}

.about-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  font-size: clamp(13px, 1.4vw, 16px);
  color: #3a3226;
}

.about-col-icon {
  width: 100px;
  height: 100px;
  object-fit: contain;
  margin-bottom: 10px;
}

.col-highlight {
  color: var(--gold-dark);
  font-weight: 700;
}

/* When .col-highlight is used on a link (the FAQ note's chatbot link),
   underline it so it reads as clickable — matching .access-link's styling. */
a.col-highlight {
  text-decoration: underline;
}

a.col-highlight:hover {
  color: var(--gold);
}

/* Same tinted overlay the hero uses (see .hero-overlay), layered over the
   pattern here as extra background layers rather than a separate element —
   first-listed layer paints on top, and background paint can't affect
   layout, so nothing shifts. Text sits above it as page content. */
/* Transparent on purpose. The drifting backdrop behind the whole page shows
   straight through, so the Contact section shares the hero's moving surface —
   no second copy of the pattern to keep aligned, and no seam where one stops
   and the other starts. */
.section-alt {
  max-width: 100%;
}

.section-alt > *:not(.video-placeholder) {
  max-width: 900px;
  margin-left: auto;
  margin-right: auto;
}

/*
 * ===== THE -10px LIFT IS GONE =====
 *
 * `transform: translateY(-10px)` sat here, moving the whole Contact group up
 * by a hand-measured 10px. Like every other offset removed from this file it
 * paints without reflowing, so the gap it closed above was reopened below and
 * the section's real box stayed where it always was — the layout still
 * believed in a position the reader could not see.
 *
 * It existed to claw back space that flex-start was dumping at the bottom of
 * the page. .content-sections now distributes that space itself, so there is
 * nothing left for a transform to correct.
 */

/* ===== Footer ===== */
.footer {
  flex: 0 0 auto;
  text-align: center;
  padding: 10px;
  background: var(--dark);
  color: var(--white);
  font-size: 11px;
  /*
   * ===== THE OPACITY MOVED OFF THE FOOTER AND ONTO THE TEXT =====
   *
   * This was `opacity: 0.8` on the whole footer. Once the social row went
   * inside it, that faded the icons too — and a row of seven white discs at
   * 80% on a dark bar reads as disabled rather than as quiet. Opacity on a
   * parent is inherited by every descendant and cannot be undone by a child,
   * which is exactly the trap: setting the icons back to 1 does nothing.
   *
   * So the fade now sits on the copyright line, which is the thing it was
   * always for.
   */
}

.footer p {
  margin: 0;
  opacity: 0.8;
}

/* ===== Social row =====
   White discs on the dark bar, matching the reference: the disc carries the
   contrast and the mark is painted in the footer's own dark, so there is one
   colour pair rather than seven brand palettes competing with each other at
   18px. */
.social {
  display: flex;
  justify-content: center;
  align-items: center;
  /*
   * `gap` rather than margins on the links. A margin on the last child leaves
   * the row optically off-centre by half the gap, which at seven items is
   * about 5px — small enough to look like a mistake rather than read as one.
   */
  gap: 10px;
  margin-bottom: 9px;
  /* Wrap rather than overflow. At 360px seven 30px discs plus their gaps are
     wider than the bar, and a footer that scrolls sideways on a phone is
     worse than one that takes two lines. */
  flex-wrap: wrap;
}

.social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--white);
  color: var(--dark);
  /* The <a> is the disc, so the whole circle is the click target — not just
     the glyph inside it, which at 16px would fail every touch guideline. */
  text-decoration: none;
  transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease;
}

.social-link svg {
  width: 16px;
  height: 16px;
  display: block;
}

.social-link:hover,
.social-link:focus-visible {
  /* --gold-dark, not --gold. The lighter gold under a white glyph is about
     2:1 contrast; this is the pair already used elsewhere for exactly that. */
  background: var(--gold-dark);
  color: var(--white);
  transform: translateY(-2px);
}

/* Keyboard focus must be visible on a dark bar, where the browser default
   outline nearly disappears. */
.social-link:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 2px;
}

/*
 * ===== AN UNSET LINK LOOKS UNSET =====
 *
 * Paired with social.js, which also removes the href, blocks the click and
 * names the offender in the console. Dimmed rather than hidden: a missing
 * icon would leave a hole in a row of seven and read as a rendering fault,
 * where a dim one reads as unfinished, which is what it is.
 */
.social-link.is-unset {
  opacity: 0.38;
  cursor: not-allowed;
}

.social-link.is-unset:hover {
  background: var(--white);
  color: var(--dark);
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .social-link {
    transition: none;
  }
  .social-link:hover,
  .social-link:focus-visible {
    transform: none;
  }
}

/* ===== Chat widget =====
   Floating enquiry chatbot, avatar cropped from the falcon mascot. Flat
   design on purpose (borders instead of drop shadows), to match the rest
   of the page. Fixed positioning so it sits above the page regardless of
   the no-scroll layout. */
.chat-widget {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 50;
}

.chat-toggle {
  width: 88px; /* source avatar is 300px, so this stays crisp */
  height: 88px;
  border-radius: 50%;
  border: 3px solid var(--gold);
  background: var(--white);
  padding: 0;
  cursor: pointer;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.2s ease, border-color 0.2s ease;
}

.chat-toggle:hover {
  transform: scale(1.06);
  border-color: var(--gold-dark);
}

.chat-toggle img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.chat-panel {
  position: absolute;
  right: 0;
  bottom: 102px;
  width: min(320px, calc(100vw - 40px));
  height: min(420px, calc(100vh - 110px));
  background: var(--white);
  border: 1px solid #e4ded0;
  border-radius: 12px;
  display: none;
  flex-direction: column;
  overflow: hidden;
}

.chat-panel.open { display: flex; }

.chat-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  background: var(--dark);
  color: var(--white);
  flex: 0 0 auto;
}

.chat-header-avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--gold);
}

.chat-header-text { flex: 1; min-width: 0; }

.chat-header-title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
}

.chat-header-status {
  font-size: 11px;
  opacity: 0.7;
}

.chat-close {
  background: none;
  border: none;
  color: var(--white);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
  opacity: 0.8;
}

.chat-close:hover { opacity: 1; }

.chat-messages {
  flex: 1;
  overflow-y: auto;
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: #faf8f3;
}

.chat-msg {
  max-width: 82%;
  padding: 9px 12px;
  border-radius: 10px;
  font-size: 13px;
  line-height: 1.45;
}

.chat-msg.bot {
  align-self: flex-start;
  background: var(--white);
  border: 1px solid #e4ded0;
  color: var(--dark);
}

.chat-msg.user {
  align-self: flex-end;
  background: var(--gold);
  color: var(--white);
}

.chat-msg a { color: inherit; }

.chat-input-row {
  flex: 0 0 auto;
  display: flex;
  border-top: 1px solid #e4ded0;
}

.chat-input-row input {
  flex: 1;
  border: none;
  padding: 12px 14px;
  font-size: 13px;
  font-family: var(--font);
  outline: none;
}

.chat-input-row button {
  border: none;
  background: var(--gold);
  color: var(--white);
  width: 46px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.2s ease;
}

.chat-input-row button:hover { background: var(--gold-dark); }

/* ===== Mobile ===== */
@media (max-width: 600px) {
  .nav-links { gap: 16px; }
  .nav-links a { font-size: 12px; }

  .chat-panel {
    right: -10px;
    width: calc(100vw - 24px);
  }

  .about-columns {
    flex-direction: column;
    gap: 10px;
  }
}

/* ===== Special-offer countdown =====
   Fills the band between the call to action and the first section, which was
   bare damask. Transparent rather than a card: the pattern behind it is the
   brand, and boxing this off would make it read as an advert dropped onto the
   page instead of part of it. */
/* ===== CENTRED IN ITS OWN GAP, BY CONSTRUCTION =====
 *
 * MEASURED, the band's top edge sat at 358 and the button's bottom at 359 — a
 * one-pixel OVERLAP, with 26px below it and 64px of slack pooling uselessly at
 * the bottom of the column. It read as jammed against the button because it
 * was touching it.
 *
 * Two causes, both of them arithmetic rather than taste:
 *
 *   ASYMMETRIC PADDING. 10px above and 14px below meant the band could never
 *   look centred no matter what the margins did — the content inside it sat
 *   2px high of its own middle.
 *
 *   THE CONTAINER STARTS ABOVE THE BUTTON. .content-sections carries
 *   margin-top:-30px, so its top edge is 8px past the button's bottom once its
 *   own 14px of padding is counted. A margin-top here is therefore 8px shorter
 *   on screen than it reads in the file, which is exactly the sort of thing
 *   that gets "fixed" by nudging a number until it looks right.
 *
 * So the 8px is cancelled explicitly and the two gaps are stated as one
 * number. GAP + 8 above, GAP below — equal on screen, and if anything above
 * this ever moves, the only value to re-solve is the 8.
 *
 * WHY NOT flex:1 AND align-items:center, which would centre it in whatever
 * space exists at any height: because the 64px of slack lives at the BOTTOM of
 * the column, so absorbing it here would push the film and the chat line down
 * by 64px — and both are positioned by fixed-pixel transforms solved against
 * their current positions. That is the bug that put the chat line through the
 * footer once already. The gap above this band does not change with viewport
 * height, so centring it is a calculation, not a layout mode.
 */
.offer-band {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: 6px 14px;
  padding: 12px 8%;
  /*
   * ===== THE TWO NUMBERS ARE NOT THE GAP. THEY SOLVE FOR IT. =====
   *
   * Neither margin equals what you see on screen, because two other things sit
   * in the same space and both were missed on the first attempt:
   *
   *   ABOVE   .content-sections has margin-top:-30px, so its top edge is
   *           already 8px past the button's bottom once its 14px of padding
   *           is counted. A margin here is 8px shorter than it reads.
   *
   *   BELOW   #about carries 28px of its own padding-top before its heading.
   *           That padding is the real spacer under the band, and no margin
   *           here can be smaller than it without going negative.
   *
   * So:  visible above = marginTop - 8      visible below = marginBottom + 28
   *
   * Re-solved to 23 / -17 once the hero stopped absorbing space (see
   * .cta-button). Measured across simulated viewport heights afterwards: 11
   * above, 11 below, at 919, 1100, 1330 and 1600. That is the check that
   * matters — an earlier pass that ignored the 28px produced 17 and 44 while
   * looking perfectly reasonable in the file, and the pass before THAT was
   * measured at a single viewport height and missed the hero entirely.
   *
   * THE FOOTPRINT IS DELIBERATELY UNCHANGED. 21 + 50 + (-15) = 56, against the
   * 55 the old band occupied. Everything below — the film and the chat line —
   * is positioned by fixed-pixel transforms solved against those positions, so
   * growing this band is what once pushed the chat line through the footer.
   * One pixel is inside the tolerance; thirty would not have been.
   *
   * WHY NOT flex:1 AND CENTRE, so it self-solves at any height: the column's
   * spare space pools at the BOTTOM, not here, so absorbing it would move the
   * film and the note by the full amount. The gap above this band is fixed at
   * every viewport height, which makes centring a calculation rather than a
   * layout mode.
   */
  margin: 23px auto -17px;
  max-width: 900px;
  width: 100%;
}

.offer-band[hidden] { display: none; }

/* ===== THE HEADING BELOW HAS TO STOP CLIMBING INTO THIS SPACE =====
   #about h2 carries margin-top: -70px, added long before this band existed to
   drag "3 Impact Tools" up and close the empty gap under the call to action.
   That is the exact gap the countdown now occupies, so the two were drawn on
   top of each other — the band laid out correctly at 428-477 and the heading
   was pulled back up to 439, straight through it.

   Cancelled only WHILE THE BAND IS ON SCREEN. Written as a sibling rule rather
   than by editing the -70px away, because the band removes itself the moment
   the offer expires: on 2 December the countdown vanishes, this rule stops
   matching, and the original spacing comes back by itself instead of leaving a
   seventy-pixel hole nobody is around to notice. */
.offer-band:not([hidden]) + #about h2 {
  margin-top: 0;
}

.offer-lede {
  font-size: clamp(12px, 1.4vw, 14px);
  letter-spacing: 0.4px;
  color: var(--dark);
  font-weight: 600;
}

.offer-date {
  font-size: clamp(12px, 1.4vw, 14px);
  color: var(--gold-dark);
  font-weight: 600;
}

/* The clock proper. Tabular figures so the seconds column does not shuffle the
   whole row sideways every tick — proportional digits make a running timer
   visibly twitch, which reads as a bug. */
.offer-clock {
  display: inline-flex;
  align-items: baseline;
  gap: 2px;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
}

.offer-unit {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
}

.offer-unit b {
  font-size: clamp(15px, 2vw, 19px);
  font-weight: 700;
  color: var(--dark);
  min-width: 1.6ch;
  text-align: right;
}

.offer-unit i {
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--gold-dark);
}

.offer-sep {
  color: var(--gold-dark);
  opacity: 0.5;
  padding: 0 2px;
}

/* Short viewports have no room to spare — the whole page is locked to 100vh
   with overflow hidden, so this band has to give ground before the sections
   below it do. */
/* The 40vh hero used to overflow below ~793px tall, pushing the button through
   the countdown band. .hero now sizes to content at every height, so the
   override that used to live here is unnecessary. */

@media (max-height: 620px) {
  /* Symmetric, matching the band's normal padding — the asymmetric 4/8 here
     predated the centring work and would have tilted it again at this size. */
  .offer-band { padding: 6px 8%; gap: 2px 10px; }
  .offer-unit i { display: none; }
}

/* ================================================================
   THE RESOURCE RAIL — five other places this work lives

   Sits in the space under the film that the removed liner gave back.

   ===== WHY AN ARC AND NOT A ROW =====

   The space is wide and short: roughly 230px of run between the columns
   above and the footer, most of which the film takes. Six equal cards in a
   row at that height are all too small to read. An arc spends the height on
   ONE card and lets the rest recede, so the front card is legible at a
   glance and the others read as "there is more here" — which is the whole
   job of this element.

   ===== WHY CSS 3D AND NOT THREE.JS =====

   This is five link cards on a marketing page with no build step and no
   framework. React Three Fiber would have meant several hundred kilobytes of
   React and Three.js, a bundler this site does not have, and a WebGL context
   on a page whose entire budget is one viewport — to draw what `perspective`
   and `rotateY` do natively in a few lines. The reference image is a card
   ring, and a card ring is what CSS transforms are for.
   ================================================================ */

.reslink {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  /* A minimum, not a gap. See the note above #about h2. */
  margin-top: 14px;
  /*
   * Sized in vh as well as px so a short window shrinks the rail instead of
   * pushing it under the footer, where the page's overflow:hidden would cut
   * it in half without a scrollbar to reveal it. That failure has already
   * happened once on this page — see the film's comment.
   */
  height: clamp(84px, 13vh, 112px);
}

.reslink-stage {
  position: relative;
  flex: 1;
  height: 100%;
  max-width: 560px;
  /*
   * The vanishing point. Larger than the card depth on purpose: a tight
   * perspective throws the side cards into a keystone so severe the text
   * stops being readable, which turns a navigation element into decoration.
   */
  perspective: 900px;
  perspective-origin: 50% 50%;
}

.reslink-ring {
  position: absolute;
  inset: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  transform-style: preserve-3d;
}

/*
 * One slot per card. `--d` is how many steps this card sits from the front,
 * signed, and is written by resources.js on every move. Everything below is
 * derived from it, so the script sets one number and the stylesheet owns the
 * entire look — no inline styles carrying design decisions.
 */
.reslink-slot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: clamp(132px, 20vw, 172px);
  height: clamp(72px, 11vh, 96px);
  margin: 0;
  transform-style: preserve-3d;
  transition:
    transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1),
    opacity 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
  transform:
    translate(-50%, -50%)
    translateX(calc(var(--d, 0) * 52%))
    translateZ(calc(var(--d, 0) * var(--d, 0) * -46px))
    rotateY(calc(var(--d, 0) * -34deg));
}

/*
 * Depth banding rather than a formula, because opacity that falls off
 * smoothly leaves the third card faintly visible behind the second and the
 * arc reads as smudged. Three clean steps and then nothing.
 */
.reslink-slot[data-depth="0"] { opacity: 1;    z-index: 4; }
.reslink-slot[data-depth="1"] { opacity: 0.62; z-index: 3; }
.reslink-slot[data-depth="2"] { opacity: 0.26; z-index: 2; }
.reslink-slot[data-depth="3"] { opacity: 0;    z-index: 1; }

.reslink-card {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  /*
   * Text-only is the default layout. A card with a thumbnail switches to a row
   * via .has-thumb below — written that way round because most cards will not
   * have one for a while, and because a broken image path removes the class at
   * runtime and has to land back on something that works.
   */
  width: 100%;
  height: 100%;
  padding: 10px 14px;
  text-decoration: none;
  text-align: center;
  border-radius: 10px;
  /*
   * TRANSPARENT, as asked. The damask backdrop drifts behind the whole page,
   * so a solid card would punch five opaque holes in a surface that is meant
   * to be continuous. The wash is just enough to hold the text against a
   * moving pattern.
   */
  background: rgba(255, 253, 249, 0.42);
  backdrop-filter: blur(7px) saturate(1.15);
  -webkit-backdrop-filter: blur(7px) saturate(1.15);
  border: 1px solid rgba(201, 165, 74, 0.55);
  box-shadow:
    0 10px 26px rgba(20, 16, 8, 0.14),
    inset 0 1px 0 rgba(255, 255, 255, 0.55);
  transition: border-color 180ms ease, box-shadow 180ms ease, background 180ms ease;
}

.reslink-slot[data-depth="0"] .reslink-card:hover,
.reslink-card:focus-visible {
  background: rgba(255, 253, 249, 0.62);
  border-color: var(--gold);
  box-shadow:
    0 14px 32px rgba(20, 16, 8, 0.2),
    inset 0 1px 0 rgba(255, 255, 255, 0.7);
}

/* Visible on the damask, and on the card, without a second rule per state. */
.reslink-card:focus-visible {
  outline: 2px solid var(--gold-dark);
  outline-offset: 3px;
}

.reslink-label {
  font-size: clamp(13px, 1.5vw, 16px);
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--dark);
}

.reslink-blurb {
  font-size: clamp(10px, 1.05vw, 12px);
  line-height: 1.3;
  color: #6b6354;
}

/*
 * ===== A CARD WITH NO URL REFUSES TO LOOK FINISHED =====
 *
 * href="#" would have rendered as a perfectly normal card that jumps the page
 * to the top when clicked. Six of those could sit on the homepage looking
 * correct until somebody happened to press one. This is deliberately visible
 * from across the room.
 */
.reslink-card.is-unset {
  border-style: dashed;
  border-color: rgba(169, 130, 47, 0.45);
  cursor: not-allowed;
}
.reslink-card.is-unset .reslink-label { color: #8a8272; }
.reslink-card.is-unset .reslink-blurb { font-style: italic; }

.reslink-arrow {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  font-size: 20px;
  line-height: 1;
  color: var(--gold-dark);
  background: rgba(255, 253, 249, 0.5);
  border: 1px solid rgba(201, 165, 74, 0.45);
  border-radius: 50%;
  cursor: pointer;
  transition: background 160ms ease, transform 160ms ease;
}

.reslink-arrow:hover { background: rgba(255, 253, 249, 0.8); transform: scale(1.08); }
.reslink-arrow:focus-visible { outline: 2px solid var(--gold-dark); outline-offset: 2px; }

/*
 * ===== REDUCED MOTION REMOVES THE MOVEMENT, NOT THE LINKS =====
 *
 * The arc stays, so the layout does not jump between preferences. What goes
 * is the travel: transitions become instant here and resources.js never
 * starts its timer. Arrows, arrow keys and drag all still work, so every one
 * of the five is still reachable — a motion preference should cost an
 * animation, never a destination.
 */
@media (prefers-reduced-motion: reduce) {
  .reslink-slot { transition: none; }
  .reslink-card { transition: none; }
  .reslink-arrow { transition: none; }
}

/*
 * Narrow screens: the side cards would overlap the arrows and each other, so
 * the arc flattens toward a single card with a hint of the next.
 */
@media (max-width: 560px) {
  .reslink-slot {
    transform:
      translate(-50%, -50%)
      translateX(calc(var(--d, 0) * 62%))
      translateZ(calc(var(--d, 0) * var(--d, 0) * -60px))
      rotateY(calc(var(--d, 0) * -26deg));
  }
  .reslink-slot[data-depth="2"] { opacity: 0; }
}

/* ===== The chatbot's standing menu bar =====
 *
 * One link, always there, above the conversation rather than in it. Built as
 * a flex row because a second may be added; the Employer Challenge is
 * deliberately not that second one — see the comment in index.html.
 *
 * A link offered as a chat bubble only appears if the visitor happens to type
 * a matching keyword, and scrolls out of sight once the conversation is a few
 * messages long. This is visible from the moment the panel opens and stays
 * visible, which is what a route to a form needs to be.
 */
.chat-menu {
  /*
   * ===== WITHOUT THIS THE BAR VANISHES, AND IT DID =====
   *
   * .chat-panel is a flex column with a fixed height — min(420px, 100vh-110px)
   * — and `overflow: hidden`. Its other four children (header, messages, FAQ
   * chips, input row) all declare their flex behaviour. This one did not, so
   * it took the default `flex: 0 1 auto`: allowed to SHRINK.
   *
   * .chat-messages is `flex: 1`, which means a flex-basis of 0. When the panel
   * runs out of room, shrinkage is distributed in proportion to basis — and a
   * basis of 0 contributes nothing, so the messages absorbed none of it and
   * this bar absorbed all of it. It collapsed to zero height and the two
   * survey links disappeared, on a panel that otherwise looked completely
   * normal.
   *
   * It got worse the day a second link was added, because two pills wrap to
   * two rows and ask for more height than one.
   *
   * 0 0 auto: never grow, NEVER SHRINK, size to content. The same declaration
   * the header and the input row already carried.
   */
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px 10px;
  background: #f3efe4;
  border-bottom: 1px solid rgba(201, 165, 74, 0.35);
}

.chat-menu-item {
  flex: 1 1 auto;
  padding: 5px 9px;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.25;
  text-align: center;
  text-decoration: none;
  color: var(--gold-dark);
  background: rgba(255, 255, 255, 0.75);
  border: 1px solid rgba(201, 165, 74, 0.5);
  border-radius: 999px;
  transition: background 160ms ease, color 160ms ease;
}

.chat-menu-item:hover {
  background: var(--gold);
  color: #fff;
}

/* Gold on cream is legible but low-contrast, so focus needs its own ring
   rather than relying on the hover colour to double as one. */
.chat-menu-item:focus-visible {
  outline: 2px solid var(--gold-dark);
  outline-offset: 2px;
}

/* ===== The five FAQ chips, directly above the text field =====
 *
 * THE BUG THIS BLOCK EXISTS TO PREVENT. The first version laid the five out in
 * a single non-wrapping row. The panel is 320px wide. Options four and five
 * were therefore off the right-hand edge, with no scrollbar and no arrow to
 * say so, and the owner asked the only sensible question about it: how is
 * anyone meant to reach them.
 *
 * So: `flex-wrap: wrap` and NO `flex: 1 1 auto`. Growing each chip to fill its
 * line is what forced the row wider than the panel in the first place. These
 * size to their text and drop onto the next line when they run out of room.
 * All five are visible the moment the panel opens, on the narrowest phone,
 * with no gesture to discover.
 *
 * `max-height` with scrolling is the backstop for a translation or an
 * over-long label a future editor adds: the bar takes at most about half the
 * panel and the conversation keeps the rest. If you find yourself raising it,
 * shorten the label instead — chat.js keeps the full question in `question`,
 * so a short chip still asks and logs the real thing.
 */
.chat-faq {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  padding: 8px 10px;
  background: #f3efe4;
  border-top: 1px solid rgba(201, 165, 74, 0.35);
  max-height: 132px;
  overflow-y: auto;
  flex: 0 0 auto;
}

.chat-faq-btn {
  padding: 5px 9px;
  font-family: inherit;
  font-size: 11px;
  font-weight: 600;
  line-height: 1.25;
  text-align: left;
  color: var(--gold-dark);
  background: rgba(255, 255, 255, 0.75);
  border: 1px solid rgba(201, 165, 74, 0.5);
  border-radius: 999px;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease;
}

.chat-faq-btn:hover {
  background: var(--gold);
  color: #fff;
}

.chat-faq-btn:focus-visible {
  outline: 2px solid var(--gold-dark);
  outline-offset: 2px;
}

/* ===== PULLING THE FILM AND THE RAIL UP OUT OF THE FOOTER =====
 *
 * MEASURED IN THE BROWSER, not calculated. Before this block, at a 941px
 * viewport:
 *
 *     about columns end   612
 *     film                674 - 790
 *     rail                804 - 916      <- 10px of it under the footer
 *     #contact ends       944            <- 38px past its own container
 *     footer starts       906
 *
 * The rail was being cut in half by the page's own overflow:hidden, with no
 * scrollbar to reveal it. Exactly the failure the film's comment records
 * happening to its caption, repeated on the element that replaced it.
 *
 * ===== WHY PADDING AND NOT A TRANSFORM =====
 *
 * The instinct here, and what this file did before, is translateY on the film.
 * That paints without reflowing, so the rail underneath does NOT follow it and
 * the gap between them has to be hand-solved a second time — which is how
 * every "re-solved" comment in this file came to exist.
 *
 * Padding moves the film and takes the rail with it, because they are in
 * normal flow. Nothing above moves: #about's padding-bottom sits BELOW the
 * columns, so trimming it lifts everything after it while the columns stay
 * exactly where they are. The hero, the countdown and the three columns are
 * untouched, which was the requirement.
 *
 * AFTER, measured the same way:
 *
 *     about columns end   612
 *     film                638 - 754      <- 36px higher
 *     rail                768 - 880      <- 36px higher, clear of the footer
 *     footer starts       906
 *
 * 26px above the film and 26px below the rail. Even, and even on purpose:
 * an earlier pass had 22 above and 30 below, which reads as the group having
 * slipped downward rather than having been placed.
 */
#about {
  /* Below the columns, so this lifts what follows without moving them. */
  padding-bottom: 16px;
}

#contact {
  padding-top: 12px;
  padding-bottom: 12px;
}

/*
 * 16px -> 8px. Small, and the reason it is not zero: at zero the film's top
 * border sits hard against the section boundary and reads as clipped rather
 * than as placed.
 *
 * A minimum, not a gap. See the note above #about h2.
 */
.video-placeholder {
  margin-top: 8px;
}

/* ===== A CARD WITH A THUMBNAIL =====
 *
 * Image on the left, label and blurb stacked beside it. A row rather than a
 * column because the card is wider than it is tall: stacking a 44px image on
 * top of two lines of text leaves the text about 40px to live in, which is
 * where the blurb stops being readable and starts being texture.
 *
 * OPT-IN PER CARD. .has-thumb is added only when a `thumb` path is set, so a
 * card without one keeps the layout it has today and the two sit side by side
 * in the same arc without either looking unfinished.
 */
.reslink-card.has-thumb {
  flex-direction: row;
  align-items: center;
  gap: 10px;
  text-align: left;
  padding: 10px 12px;
}

.reslink-thumb {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: 8px;
  /*
   * The border matters more here than on an ordinary avatar. These cards are
   * translucent and sit on a moving damask, so an image with a pale edge
   * dissolves into the background and reads as a smudge rather than a picture.
   */
  border: 1px solid rgba(201, 165, 74, 0.5);
  background: rgba(255, 255, 255, 0.6);
}

/*
 * The text column. Only meaningful once there is something beside it, so it is
 * transparent to layout in the text-only case rather than adding a wrapper the
 * default card has to route around.
 */
.reslink-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0; /* lets a long label wrap instead of pushing the thumb out */
}

.reslink-card.has-thumb .reslink-label { line-height: 1.15; }
.reslink-card.has-thumb .reslink-blurb { line-height: 1.25; }

/* On a narrow screen the card loses ~30px of width, which is exactly the
   thumbnail plus its gap. The image goes and the text gets the room back. */
@media (max-width: 560px) {
  .reslink-thumb { display: none; }
  .reslink-card.has-thumb { flex-direction: column; text-align: center; }
}

/* ===== SPACING AFTER THE COUNTDOWN BAND CAME OUT =====
 *
 * The band occupied the run between the hero and the first section. Removing it
 * did not leave a surplus — with every negative offset also removed the page
 * ran 33px past the footer — so this is a redistribution rather than a windfall.
 * The numbers are what four measured trials in Chrome landed on; see #about h2
 * for the resulting gaps.
 */
/*
 * ===== THE MEASURED PIXELS SURVIVE, AS MINIMUMS =====
 *
 * The four measured trials described above are still what a short window
 * renders. They are just no longer the whole answer: the rule below makes both
 * sections flexible so any spare height is spread through the page instead of
 * piling up under the rail, and these numbers become the floor that spread
 * collapses to when there is nothing spare.
 */
#about {
  padding-top: 20px;
  padding-bottom: 8px;
}

#contact {
  padding-top: 6px;
}

/*
 * ===== WHERE THE SPARE HEIGHT ACTUALLY GOES =====
 *
 * .section is flex: 0 0 auto, so every section was exactly its content height
 * and all the slack on a tall window ended up in one lump under the last one —
 * about 150px of dead space beneath the resource rail, with the headline,
 * columns, film and rail packed hard against each other above it. The page was
 * never short of room. The room was in the one place nothing needed it.
 *
 * flex: 1 1 auto lets both sections take a share of that slack, and
 * space-evenly spreads each section's share through the gaps BETWEEN its own
 * children — so the heading, the columns, the film and the rail all get some,
 * rather than the space being handed to the footer.
 *
 * WHY THIS CANNOT OVERFLOW THE WAY THE vh CLAMPS DID. Flex distribution has
 * nothing to hand out when free space is zero or negative, so a short window
 * collapses to precisely the measured minimums above. Growth is conditional
 * here; a vh term's growth is not. That is the whole difference.
 */
.content-sections > .section {
  flex: 1 1 auto;
  justify-content: space-evenly;
}

/*
 * ===== THE SHORT-WINDOW TIGHTENING IS GONE, AND ITS JOB WITH IT =====
 *
 * `@media (max-height: 900px)` sat here, squeezing #about and #contact to claw
 * back ~24px of headroom on a short window. It was written when a short window
 * still had to fit the locked layout, and its own comment admitted it could not
 * finish the job: "at 700px tall it is off by 178px... needs a decision rather
 * than another 20px."
 *
 * That decision has now been made, twenty lines further down — under 920px of
 * height the page scrolls. So this block's entire domain is now inside the
 * scrolling one, where there is no shortage to claw back from. Left in place it
 * would only cramp the spacing of a page that has all the room it needs.
 *
 * Deleted rather than kept "just in case": a rule whose reason has gone is the
 * next person's puzzle, and this file already carries five comments explaining
 * offsets that outlived the thing they were solved against.
 */

/* ================================================================
   THE PHONE GETS A SCROLLING PAGE
   ================================================================

   ===== WHAT WAS ACTUALLY HAPPENING, MEASURED AT 375x812 =====

   footer            777 - 812     pinned to the bottom of the viewport
   icon columns      403 - 865     ending 88px BELOW the footer
   the film          879 - 943     entirely below it
   the resource rail 957 - 1062    entirely below it

   1062px of content in an 812px viewport, with `overflow: hidden` and
   therefore NO SCROLLBAR. On a phone the film and all five resource links did
   not exist, and the third icon column was sliced through the middle by the
   footer. The page looked deliberate the whole time — it simply ended early.

   ===== WHY MOBILE FAILS SO MUCH HARDER THAN A SHORT DESKTOP WINDOW =====

   The three icon columns are a flex row that switches to a column below 600px.
   Stacked, they are 462px tall instead of 155px. That one change adds ~300px
   to a page whose entire premise is that it fits on one screen, which is why
   no amount of gap tuning reaches this: the content is simply taller than the
   device, and correctly so.

   ===== SO THE PREMISE IS RELEASED HERE, AND ONLY HERE =====

   Desktop keeps the locked single viewport exactly as it was. Below 768px the
   page becomes an ordinary scrolling document. Nothing is hidden and nothing
   is shrunk to make it fit — a phone visitor sees the same page a desktop
   visitor does, in the order it was written, and scrolls.

   768px RATHER THAN 600px, even though the columns only stack at 600. Height
   is auto here, so if the content does fit there is no scrollbar and this
   changes nothing visible. Releasing early is free; releasing late leaves the
   601-767px band silently cutting content, which is the bug being fixed.

   ===== THE THREE PROPERTIES THAT HAVE TO GO TOGETHER =====

   html/body overflow:hidden   what removes the scrollbar
   html/body height:100%       what caps the page at the viewport
   body height:100vh           the same cap again, on the flex container

   Releasing any two of the three leaves the page still clipped, with nothing
   to say which one is still holding. */
/* ===== TOO NARROW, *OR* TOO SHORT =====
 *
 * The width half fixes the phone. The height half fixes the bug that was
 * recorded and left alone when the phone was done: a desktop window under
 * ~900px tall clips the resource rail exactly the same way, because it is
 * above the width breakpoint and the locked layout still applies there. At
 * 700px tall the rail ran 178px past the footer, cut with no scrollbar.
 *
 * ONE CONDITION, NOT TWO FIXES. The failure was never about phones. It is
 * "the content is taller than the viewport and the page refuses to scroll",
 * and a device is only one way to arrive at it — a half-height browser window
 * on a laptop is another, and a visitor with a large system font is a third.
 * Stating the condition rather than the device is what makes this cover the
 * cases nobody has thought of yet.
 *
 * ===== 900px, AND THE NUMBER WAS MEASURED RATHER THAN REASONED =====
 *
 * The first attempt used 920, on the arithmetic that the locked layout has 17px
 * of slack at 919 and therefore runs out around 902. That arithmetic was wrong,
 * because the film and the rail are both sized in vh: a shorter window shrinks
 * them, which buys back some of the height it just took away. Slack does not
 * fall 1:1 with viewport height, so it cannot be extrapolated from one reading.
 *
 * Measured instead, locked layout, rail-to-footer slack:
 *
 *     960px  ->  24px          905px  ->  14px
 *     930px  ->  19px          880px  ->   2px
 *                              840px  -> -35px   (clipped)
 *
 * It survives to about 878. So 900 is not "just below the last value that
 * worked" — it is above the real cliff with ~13px in hand, while leaving every
 * common laptop height on the distributed layout rather than switching them to
 * a scrolling one they do not need. 920 would have moved a 919px window off the
 * better layout to solve a problem that starts 40px lower down.
 *
 * Height is auto here, so where the content already fits there is no scrollbar
 * and none of this is visible. It costs nothing to release early. */
@media (max-width: 768px), (max-height: 900px) {
  html,
  body {
    height: auto;
    overflow: visible;
  }

  /*
   * .content-sections is flex:1 and its sections flex:1 1 auto with
   * space-evenly — right for a fixed-height column, wrong for a scrolling one.
   * On a page that can grow, "distribute the spare height" has no spare height
   * to distribute and instead stretches the sections to whatever the flex
   * container happens to be. Back to content height, stacked from the top.
   */
  .content-sections {
    flex: 0 0 auto;
    justify-content: flex-start;
    overflow: visible;
    padding-bottom: 28px;
  }

  .content-sections > .section {
    flex: 0 0 auto;
    justify-content: flex-start;
  }

  /*
   * max-height: 16vh GOES WHEREVER THE PAGE SCROLLS, and finding out why took
   * measuring the box rather than reading the rule. On the phone, widening the
   * film alone gave a 315x130 letterbox that cropped the poster's own title off
   * the bottom — while the computed aspect-ratio still cheerfully reported
   * "16 / 9". An explicit height simply wins over aspect-ratio, so the ratio was
   * being declared and ignored at the same time.
   *
   * The cap exists to stop the film pushing the rail through the footer on a
   * short LOCKED window. Once the page scrolls there is nothing left for it to
   * protect, and on a short wide window it would letterbox the film the same
   * way it did on the phone.
   */
  .video-placeholder { max-height: none; }
}

/* ===== PHONE SIZING, WHICH IS A SEPARATE QUESTION FROM SCROLLING =====
 *
 * Everything above is the mechanism: the page scrolls. This is presentation,
 * and it belongs to narrow screens only — a short but wide desktop window
 * scrolls without wanting a phone's proportions.
 *
 * The film is capped at min(206px, 30vw) so it stays small beside a desktop
 * column. On a 375px phone that is 113px wide, a video the size of a postage
 * stamp. With room to scroll it can be the width of the page. */
@media (max-width: 768px) {
  .video-placeholder {
    max-width: min(340px, 88vw);
    margin-top: 22px;
  }

  .reslink { margin-top: 24px; }
}

/* ===== THE CHAT WIDGET WAS SITTING ON THE BUTTONS =====
 *
 * An 88px avatar 20px off the corner covered the "Sign up" button on join.html
 * and the right-hand cards of the resource rail on the home page. A floating
 * widget over the primary call to action does not announce itself as a
 * problem: the tap simply opens the chat, and the sign-up never happens.
 *
 * Smaller and further into the corner. The panel's offset is derived from the
 * button's size, so it has to move with it or it opens overlapping its own
 * toggle — the same class of fixed offset this file keeps having to re-solve,
 * caught here before it shipped rather than after. */
@media (max-width: 768px) {
  .chat-widget {
    right: 12px;
    bottom: 12px;
  }

  .chat-toggle {
    width: 58px;
    height: 58px;
    border-width: 2px;
  }

  .chat-panel { bottom: 68px; }
}

/* ===== AND ON THE SIGN-UP PAGE THE FLOATING BUTTON GOES ENTIRELY =====
 *
 * Shrinking it to 58px and tucking it into the corner was not enough, which
 * the measurement said and the eye did not: the Sign up button spans the full
 * width of a phone (25px to 350px), so a bottom-right widget of ANY size lands
 * on it. There is no size that fixes a collision with a full-bleed control.
 *
 * This page has exactly one job. A floating chat bubble sitting on the only
 * button that matters does not read as a mistake to the visitor — the tap just
 * opens a chat window, and the sign-up quietly does not happen. That is a cost
 * with no trace in any log.
 *
 * ASCENT IS NOT REMOVED, ONLY ITS BUTTON. The "Enquire" link in this page's own
 * nav bar calls the same openPanel() — chat.js binds #navEnquire independently
 * of #chatToggle — so the chatbot is still one tap away, from the top of the
 * page where nothing is competing with it. The panel is repositioned to the
 * corner because its offset was measured from the toggle that is now gone.
 *
 * THE HOME PAGE KEEPS ITS FALCON. Nothing at its bottom-right corner is a
 * conversion control, so the same widget costs nothing there. */
@media (max-width: 768px) {
  .join-page .chat-toggle { display: none; }
  .join-page .chat-panel { bottom: 0; }
}

/* ===== The StrataGEM Digital Planner block =====
 *
 * Between the film and the resource rail. Added 17 September 2026.
 *
 * ===== IT BORROWS .cta-button'S LOOK BUT NOT ITS CLASS =====
 *
 * The hero's "Click to Advance Your Career" and this button are the only two
 * gold buttons on the page, and they must not become one class. The hero's
 * job is the free sign-up; this one's is a paid product. The day either
 * changes — a different colour to separate the paid offer, a size change for
 * the hero on mobile — a shared class means changing both and noticing too
 * late. Same treatment today, two names, on purpose.
 *
 * The transform on hover is the same 2px lift as the hero, so the page has one
 * motion vocabulary rather than two.
 */
.planner {
  text-align: center;
  /* Sits closer to the film than to the rail: it belongs to the argument the
     film just made, not to the list of places to go next. */
  margin: 22px auto 34px;
}

.planner-title {
  /* Matches the section headings above rather than the hero — this is a
     product name inside the page, not a second entrance. */
  font-size: clamp(17px, 2.4vw, 22px);
  font-weight: 700;
  letter-spacing: 0.5px;
  margin: 0 0 12px;
}

.planner-btn {
  display: inline-block;
  padding: 9px 30px;
  background: linear-gradient(135deg, var(--gold), var(--gold-dark));
  color: var(--white);
  text-decoration: none;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 1px;
  text-transform: uppercase;
  border-radius: 4px;
  transition: transform 0.2s ease;
}

.planner-btn:hover {
  transform: translateY(-2px);
}

/* Gold on gold is a weak focus ring, so it gets its own. */
.planner-btn:focus-visible {
  outline: 2px solid var(--dark);
  outline-offset: 3px;
}

/*
 * ===== A MOTION PREFERENCE COSTS THE LIFT, NEVER THE DESTINATION =====
 *
 * The rail already follows this rule and the button must too — an animation
 * removed is a preference honoured, a link removed is a product nobody can
 * buy.
 */
@media (prefers-reduced-motion: reduce) {
  .planner-btn { transition: none; }
  .planner-btn:hover { transform: none; }
}

/* ============================================================================
 * THE HOME PAGE'S VERTICAL RHYTHM — ONE BLOCK, AND IT IS THE LAST WORD
 * ============================================================================
 *
 * Added 17 September 2026 after the owner reported the layout as "horrendous"
 * in Safari and different again in Chrome. Both were true, and neither was a
 * browser bug.
 *
 * ===== WHAT WAS ACTUALLY WRONG =====
 *
 * Nothing in this file said how far apart the blocks on the home page should
 * be. The spacing was the sum of whatever each rule happened to declare, and
 * the same selectors had been re-declared repeatedly by later patches:
 *
 *     .video-placeholder   declared FOUR times
 *     #about               three times
 *     .reslink             twice
 *     .content-sections    twice
 *
 * So the gaps measured, in Chrome at 614px:
 *
 *     wordmark -> career liner      24
 *     career liner -> CTA           35     <- the owner's complaint
 *     CTA -> "Your Career is..."    24
 *     heading -> three columns      10
 *     columns -> film               36
 *     film -> planner title         22
 *     planner title -> buy button    8
 *     buy button -> carousel        58     <- the owner's complaint
 *
 * No rhythm, and the two worst numbers were each the SUM OF TWO MARGINS from
 * different rules: 20+15 for the CTA, and 34+24 for the carousel. Nobody chose
 * either number; they accumulated.
 *
 * ===== WHY SAFARI AND CHROME DISAGREED =====
 *
 * .content-sections and .section are flex containers, and the space came from
 * a mixture of container PADDING and child MARGIN, with the film carrying an
 * aspect-ratio and a max-height. Given `flex: 0 1 auto` children and a
 * percentage-padding container, the two engines resolve the available height
 * in different orders, so identical CSS produced different gaps. That is not a
 * thing to fix by nudging until one browser looks right — it is a thing to
 * stop depending on.
 *
 * ===== THE RULE, AND IT IS THE WHOLE POINT =====
 *
 * 1. TWO SPACING VALUES. --home-gap between blocks, --home-gap-tight inside a
 *    pair that belongs together (a heading and the thing it labels).
 * 2. THE FOLLOWING ELEMENT OWNS THE GAP. Every block sets margin-top and
 *    margin-bottom: 0. Two margins can then never stack, which is what
 *    produced both numbers the owner objected to.
 * 3. NO VERTICAL PADDING ON THE FLEX CONTAINERS. Space comes from child
 *    margins only, so neither engine has a flex calculation to disagree about.
 *
 * ===== IF YOU ARE HERE TO CHANGE A GAP =====
 *
 * Change --home-gap or --home-gap-tight. Do NOT add a fifth .video-placeholder
 * rule below this block — that is precisely how the page got into the state
 * this repairs. There is a test (test/home-rhythm.test.mjs) that counts how
 * many times these selectors are declared and fails when the count grows.
 * ============================================================================ */

:root {
  --home-gap: 26px;
  --home-gap-tight: 10px;

  /*
   * ===== THE FOOTER IS NOT A CONTENT BLOCK, SO IT GETS ITS OWN GAP =====
   *
   * Added 17 September 2026: the owner reported the carousel as "blocked by
   * the bottom icons" and asked for it higher.
   *
   * MEASURED FIRST, AND IT WAS NOT AN OVERLAP. The lowest painted card sat 34px
   * clear of the footer and the arc scales with its container, so nothing was
   * actually colliding at any height. What was wrong is that 26px — correct
   * between two blocks of content on the same cream background — is not enough
   * against a DARK BAND. A colour change reads as "the page has ended", so
   * content sitting a normal gap away from it looks like it has run into it.
   *
   * That is a real distinction and not an excuse for an exception: every other
   * gap on this page separates two things of the same kind, and this one
   * separates the page from its end. So it is a named value rather than a
   * number nudged until the screenshot looked right.
   *
   * ===== I MEASURED THE WRONG THING FIRST, WHICH IS WHY THIS IS WRITTEN DOWN =====
   *
   * My first pass measured .reslink-stage, whose box is 105px tall, and
   * reported 26px of clearance. The cards inside are positioned with 3D
   * transforms and paint outside that box, so the container's clearance was
   * never the number that mattered. Measure .reslink-slot, not the stage.
   */
  --home-gap-footer: 48px;
}

/* ============================================================================
 * THE PAGE SCROLLS. AT EVERY SIZE. THIS IS THE CHROME FIX.
 * ============================================================================
 *
 * The owner reported the spacing fixed in Safari and still broken in Chrome.
 * It was not a browser difference. It was a VIEWPORT difference, and I had
 * been measuring the wrong one.
 *
 * His window is 1920x936. The tab I was measuring through was 614x805 — a
 * narrow panel, where BOTH `max-width: 768px` and `max-height: 900px` are
 * true. Every measurement I took, and every gap I confirmed, came from the
 * short-and-narrow branch. I never once saw the layout he was looking at.
 *
 * ===== WHAT THE DESKTOP BRANCH WAS DOING =====
 *
 * At 1920x936 no media query matches, so the ORIGINAL rules apply:
 *
 *     .content-sections { flex: 1; justify-content: space-evenly;
 *                         overflow: hidden; }
 *
 * That is a fixed-height column trying to fit the whole page into 100vh. It
 * was correct when this page was a hero, three columns and a film. It is not
 * correct now that there is also a product block and a carousel: measured at
 * his size, the lowest painted carousel card sat SEVENTY PIXELS BELOW THE
 * FOOTER'S TOP, clipped by `overflow: hidden`. That is the "blocked by the
 * bottom icons" he kept reporting, and it was invisible to me.
 *
 * ===== WHY THIS IS PROMOTED RATHER THAN PATCHED =====
 *
 * The repair already existed. Every rule below was sitting inside
 * `@media (max-width: 768px), (max-height: 900px)`, where it has worked
 * correctly for weeks — the block's own comment even says space-evenly is
 * "right for a fixed-height column, wrong for a scrolling one".
 *
 * The mistake was gating it on the window being small. The page stopped
 * fitting in one screen when the content grew, not when the window shrank.
 * So the mechanism is unconditional now, the media block keeps it for older
 * caches harmlessly, and there is exactly ONE layout mode at every size.
 *
 * THAT is what makes the browsers agree. Two engines resolving `flex: 1` plus
 * `overflow: hidden` plus percentage padding against a 100vh box will differ;
 * a document that simply flows and scrolls has nothing left to disagree about.
 * ============================================================================ */

html,
body {
  height: auto;
  overflow: visible;
}

.content-sections > .section {
  flex: 0 0 auto;
  justify-content: flex-start;
}

/* Space comes from the children, so the containers contribute none. */
.content-sections {
  flex: 0 0 auto;
  justify-content: flex-start;
  overflow: visible;
  padding-top: 0;
  padding-bottom: var(--home-gap-footer);
}

/*
 * ===== THE HERO'S OWN PADDING WAS ADDING 4px TO THE NEXT GAP =====
 *
 * .hero-content carried padding: 6px top / 4px bottom, so the distance from
 * the CTA button to "Your Career is a Campaign" came out at 30 rather than 26
 * — the heading's declared margin plus a padding nobody was counting. Four
 * pixels is not the point; a rhythm that is right everywhere except one place
 * is the point, because that is how nobody trusts it and starts nudging again.
 *
 * The top padding stays: it sits above the logo, inside the hero, and is not
 * part of the between-blocks rhythm.
 */
.hero-content { padding-bottom: 0; }

#about,
#contact {
  padding-top: 0;
  padding-bottom: 0;
}

/* ----- The hero ----- */

/* The logo keeps its -6px: the GIF carries transparent padding at the bottom,
   so the visible gap to the wordmark is larger than the number says. Measured
   by eye, not derived — which is why it is exempt from the rule above and
   said so here rather than looking like an oversight. */

.hero-content h1 { margin-top: 0; margin-bottom: 0; }

.hero-content p {
  margin-top: var(--home-gap);
  margin-bottom: 0;
}

/* WAS 15px, ON TOP OF THE LINER'S 20px BOTTOM MARGIN = 35. The owner asked
   for this one by name. */
.cta-button { margin-top: var(--home-gap); margin-bottom: 0; }

/* ----- "Your Career is a Campaign" and the three columns ----- */

#about h2 { margin-top: var(--home-gap); margin-bottom: 0; }

/* A heading and the thing it labels are one unit, so they sit tight. */
.about-columns { margin-top: var(--home-gap-tight); margin-bottom: 0; }

/* ----- The film, the planner, the carousel ----- */

/*
 * max-height HERE RATHER THAN IN A RULE OF ITS OWN, and that is not tidiness.
 *
 * The 16vh cap existed to stop the film pushing the carousel through the
 * footer on a short LOCKED window. Nothing is locked any more, and on a short
 * wide window the cap letterboxes the film and crops its own poster title —
 * so it has to be lifted unconditionally.
 *
 * My first attempt added `.video-placeholder { max-height: none }` as a sixth
 * separate declaration, and the duplicate-declaration tripwire in
 * test/home-rhythm.test.mjs failed — correctly. That test says in as many
 * words "do not raise the number", and the honest response to my own guard is
 * to obey it: folded into the rule that was already here, so the count is
 * unchanged and there is still one place that describes this element.
 */
.video-placeholder {
  margin-top: var(--home-gap);
  margin-bottom: 0;
  max-height: none;
}

/*
 * ===== A FLEX COLUMN, BECAUSE AN INLINE-BLOCK SITS ON A LINE BOX =====
 *
 * Moving the gap to .planner-btn's margin-top made it WORSE, not better: the
 * declared 10px rendered as 18. The button is display:inline-block, so inside
 * a normal block it sits on a line box, and that line box's strut adds its own
 * leading above the button — 8px of it at this font size. The margin was
 * correct and the container was adding to it.
 *
 * MEASURED TWICE, WRONG BOTH TIMES, IN OPPOSITE DIRECTIONS (8 then 18). That
 * is the signal that the number was never the problem and the layout model
 * was. A flex column has no line boxes: each child is a flex item and its
 * margin is the whole distance, which is the only way the value in this file
 * and the pixels on the screen can be the same thing.
 *
 * align-items: center replaces the inherited text-align, so the title and the
 * button stay centred without relying on inline layout at all.
 */
.planner {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: var(--home-gap);
  margin-bottom: 0;
}

/*
 * ===== THE GAP IS ON THE BUTTON, NOT THE TITLE — AND THAT IS RULE 2 =====
 *
 * First attempt put it on .planner-title's margin-bottom. It asked for 10px
 * and rendered 8: .planner-btn is an inline-block, so it sits on a LINE BOX
 * whose strut and baseline alignment absorb part of the margin above it. The
 * number in the file and the number on the screen disagreed by 2px, which is
 * invisible and is exactly the kind of thing that makes a stylesheet
 * untrustworthy.
 *
 * Moving it to the button's own margin-top — which is what rule 2 at the top
 * of this block says to do anyway — makes the declared value the rendered one.
 * Caught by measuring in the browser; reading the CSS would never have shown
 * it.
 */
/*
 * ===== `.planner .planner-title`, NOT `.planner-title`, AND SPECIFICITY IS WHY =====
 *
 * `.planner-title { margin-bottom: 0 }` was in this file, LAST, and lost.
 * `.section h2 { margin-bottom: 8px }` sits 1700 lines above it and wins: one
 * class plus one type beats one class, and cascade order never gets consulted
 * because specificity is decided first.
 *
 * This is the third form the same 2px took — 8, then 18, then 8 again — and it
 * is the one that matters, because the file plainly said 0 and the browser
 * plainly rendered 8. A stylesheet that lies about its own values is worse
 * than one with ugly numbers in it.
 *
 * Two classes (0,2,0) beats a class plus a type (0,1,1), so this holds without
 * an !important and without touching `.section h2`, which other headings on
 * the page still want.
 *
 * IF YOU ADD A BLOCK TO THIS RHYTHM, check the computed value in a browser
 * rather than trusting that being last is enough. It is not.
 */
.planner .planner-title { margin-top: 0; margin-bottom: 0; }
/*
 * ===== THE OWNER'S OWN +10px, KEPT VISIBLE AS AN OFFSET =====
 *
 * Asked for on 17 September 2026: "shift the Click to buy button downwards by
 * 10 px". Written as `calc(scale + 10px)` rather than as a flat `20px` so the
 * two parts stay legible — the shared value the rest of the page uses, and the
 * deliberate adjustment on top of it. A bare 20 would look like a fourth
 * spacing value invented from nowhere, which is the state this stylesheet was
 * dug out of.
 *
 * THE OFFSET DOES NOT SHRINK ON A PHONE. --home-gap-tight drops from 10 to 8
 * below 560px; the +10 does not, so the gap there is 18 rather than 20. That
 * is a judgement, not an oversight: the adjustment was made by eye on a
 * desktop, and scaling a correction somebody made by looking at one screen is
 * inventing a number they never approved. If the phone reads loose, this is
 * the line.
 */
.planner .planner-btn { margin-top: calc(var(--home-gap-tight) + 10px); }

/*
 * ===== THIS NUMBER HAS MOVED FOUR TIMES. THE HISTORY IS THE REASONING. =====
 *
 *   58  two margins stacked (34 + 24). Nobody chose it. The owner's first
 *       complaint.
 *   26  uniform with every other block. Still read as too low — but the real
 *       problem then was the distance to the dark footer band, not to the
 *       button above.
 *   10  the tight gap, pairing the carousel upward with the buy button. That
 *       LOOKED right, and it was right, because the stage was silently 47px
 *       taller than its cards: the real space above the arc was 57px.
 *   26  once that dead space was removed, 10px became 10px — and the owner
 *       said so immediately: "the spacing is too tight".
 *
 * ===== WHICH IS WHY IT IS BACK TO THE ORDINARY BLOCK GAP =====
 *
 * The tight gap is for a heading and the thing it labels — two parts of one
 * object. The carousel is not part of the buy button; it is the next block,
 * and the page has a value for that.
 *
 * THE LESSON WORTH KEEPING: 10 only ever looked correct because a container
 * was lying about its height. Any spacing tuned against a box that reserves
 * room it does not fill is tuned against nothing, and it breaks the moment
 * somebody fixes the box.
 */
/*
 * height: auto HERE, NOT IN A RULE OF ITS OWN.
 *
 * Was clamp(84px, 13vh, 112px). The cards are 16:9 now, so their height is a
 * consequence of their width and this only has to be tall enough not to clip.
 *
 * My first attempt added a separate `.reslink { height: auto }`, which both
 * pushed the duplicate-declaration count past its ceiling AND broke the
 * no-stacking rule — the last .reslink rule has to pin both margins, and a
 * height-only rule arriving after this one left the top margin to an earlier
 * declaration. Two of my own guards caught it. Folded in.
 */
/*
 * ===== AND +15px ON THE CAROUSEL, SAME REASONING =====
 *
 * "shift the carousel downwards by 15 px", same instruction. 26 + 15 = 41 at
 * desktop, 18 + 15 = 33 on a phone — again the base scales and the offset does
 * not, for the reason above.
 *
 * THIS IS THE FIFTH VALUE THIS GAP HAS HAD (58, 26, 10, 26, 41) and the first
 * one measured by the owner against a layout that was not lying about its own
 * height. The 10 only ever looked right because .reslink-stage reserved 47px
 * it did not fill; every number before this one was tuned against that.
 */
.reslink {
  margin-top: calc(var(--home-gap) + 15px);
  margin-bottom: 0;
  height: auto;
}

/* ----- Narrow screens -----
 *
 * The rhythm SHRINKS rather than changing shape. A phone has less vertical
 * room, so holding 26px everywhere pushes the carousel below the fold; giving
 * different elements different mobile gaps would reintroduce exactly the
 * per-element drift this block removes. One variable, one smaller value.
 */
@media (max-width: 560px) {
  :root {
    --home-gap: 18px;
    --home-gap-tight: 8px;
    /* Still the largest gap on the page, and proportionally larger than the
       others: a phone has less room, and the footer boundary is the one place
       where losing the separation makes the layout look broken rather than
       merely tight. */
    --home-gap-footer: 34px;
  }
}

/* ============================================================================
 * THE CAROUSEL IS A PLANNER PREVIEW NOW, SO THE CARDS HAVE TO BE BIG ENOUGH
 * ============================================================================
 *
 * Replaced five empty outbound links with five pages of the planner on
 * 17 September 2026.
 *
 * ===== WHY THE CARDS GREW =====
 *
 * The old card was clamp(132px, 20vw, 172px) wide — right for two lines of
 * text naming a website. These cards carry a 16:9 slide with a table or a
 * diagram on it. At 172px that is a smudge: the reader cannot tell a
 * comparison table from a flow chart, so the gallery says "there are some
 * pages" and nothing else.
 *
 * MEASURED, NOT GUESSED: the stage is capped at 560px and the side cards sit
 * at ±62% of the card width, so a 320px card puts its neighbours' centres at
 * ±198px — inside the stage with room for the arrows. That is the ceiling, and
 * it is where the clamp lands on a desktop.
 *
 * The height follows from 16:9 rather than being a second clamp, because the
 * slides ARE 16:9 and two independent numbers would letterbox them the first
 * time one was edited. That exact fault is recorded on .video-placeholder,
 * where an explicit height silently beat an aspect-ratio.
 *
 * ===== THE IMAGE IS THE CARD, THE LABEL SITS ON IT =====
 *
 * Not a 44px thumbnail beside text — the slide is the thing worth looking at.
 * The label stays because the slide is unreadable at this size and a caption
 * is what makes each card distinguishable; it is painted on a gradient so it
 * stays legible over whatever part of the image is behind it.
 * ============================================================================ */

/*
 * ===== THE CARD WIDTH IS A VARIABLE BECAUSE TWO RULES NEED IT =====
 *
 * The slot uses it directly; the stage needs it to work out how tall to be.
 * Written twice, they drift, and the drift shows up as dead space rather than
 * as anything that looks like a bug.
 */
:root {
  --card-w: clamp(190px, 26vw, 320px);
}

.reslink-slot {
  width: var(--card-w);
  /* 16:9, from the width. No second clamp — see above. */
  height: auto;
  aspect-ratio: 16 / 9;
}

/*
 * ===== THE STAGE WAS 47px TALLER THAN ANYTHING IN IT =====
 *
 * The owner reported "too much space between Click to buy your 2027 copy and
 * the carousel". Measured in his Chrome: the button ended at 780 and the
 * highest painted card began at 837 — a 57px gap where the rule says 10.
 *
 * The 47px was not margin. The stage was `aspect-ratio: 16 / 9` of ITS OWN
 * width, capped at 210px — and the stage is as wide as the rail lets it be
 * (up to 560px), while the cards are only 204px wide and 115px tall. So the
 * stage reserved 210px of height to hold 115px of card, and the slots, being
 * absolutely positioned at top: 50%, centred inside it. Half the surplus sat
 * above the arc and half below.
 *
 * NUDGING THE BUTTON DOWN WOULD HAVE HIDDEN IT, NOT FIXED IT. The gap would
 * have come back the moment the card size or the rail width changed, because
 * the number causing it was never written down anywhere.
 *
 * The stage is now exactly one card tall — the same 16:9 applied to the CARD's
 * width rather than the stage's. The rotated neighbours are pushed back in Z,
 * so they are smaller than the front card and cannot exceed it.
 */
.reslink-stage {
  aspect-ratio: auto;
  max-height: none;
  height: calc(var(--card-w) * 9 / 16);
}

.reslink-card {
  position: relative;
  padding: 0;
  overflow: hidden;
  /*
   * Still the slides' own cream rather than the frosted panel. Nothing
   * letterboxes now, so this is only visible in the moment before the image
   * paints — and a cream flash matches what arrives, where a translucent panel
   * showing the damask through it does not.
   */
  background: #f2efe7;
}

/*
 * ===== cover, AND THE OWNER SUPPLIED THE ARTWORK THAT MAKES IT SAFE =====
 *
 * This read `contain` for one afternoon, and the reason is worth keeping
 * because it is the reason the new cover exists.
 *
 * The first cover was 944x712 — 4:3 — in a 16:9 frame. With `cover` it lost
 * 35% of its height to the crop, and what went was the diamond and most of the
 * wordmark: the front card, the one thing that had to be showing on arrival,
 * rendered as an empty cream rectangle with a caption on it. It looked like a
 * failed image load rather than a cropped one, which is how it survived a
 * screenshot before anybody noticed.
 *
 * `contain` fixed that by letterboxing, and the owner then did the better
 * thing: re-exported the cover at 942x601. That is a 1.57 ratio against the
 * frame's 1.78, so `cover` now trims about 7% from the top and bottom — above
 * the diamond and below the subtitle, where the artwork has margin.
 *
 * SO IT FILLS THE BOX, as asked, with no letterbox bands. The four inside
 * pages are 1376x768, exactly 16:9, so `cover` and `contain` render them
 * identically — this rule cannot crop them at all.
 *
 * IF A FUTURE SLIDE IS NOT ROUGHLY 16:9 THIS WILL CROP IT. Check what the
 * trim removes before adding one, or re-export it to match.
 */
.reslink-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}

/*
 * The caption. Bottom-anchored over a gradient, because the slides are pale
 * cream at the top and pale green or pink at the bottom — text with no scrim
 * is legible on some cards and not others, which is the worst of both.
 */
.reslink-card.has-img .reslink-text {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 18px 10px 8px;
  text-align: center;
  background: linear-gradient(to top, rgba(28, 23, 12, 0.82), rgba(28, 23, 12, 0));
}

.reslink-card.has-img .reslink-label {
  font-size: clamp(11px, 1.15vw, 14px);
  line-height: 1.2;
  color: #fff;
}

.reslink-card.has-img .reslink-blurb {
  font-size: clamp(9px, 0.95vw, 11.5px);
  line-height: 1.25;
  color: rgba(255, 255, 255, 0.82);
}

/*
 * ===== THE EARLIER OVERRIDE THAT HID THE IMAGE HAS TO GO =====
 *
 * `.reslink-thumb { display: none }` and `.has-thumb { flex-direction: column }`
 * sit in a media block above, from when the thumbnail was a 44px square that
 * crowded the text on a phone. The image IS the card now, so hiding it on a
 * phone would leave five blank frosted rectangles under the buy button.
 *
 * Kept as an explicit override rather than deleted upstream: the old rules
 * name `.reslink-thumb`, which no longer exists in the markup, so they are
 * already inert — and this line is what makes that survivable if somebody
 * reintroduces the class.
 */
.reslink-card.has-img .reslink-img { display: block; }

@media (max-width: 560px) {
  /* A phone is 375px wide, so the card shrinks — and the stage follows it
     automatically now, because both read the same variable. The separate
     max-height this used to carry was the phone's copy of the same 47px
     mistake. */
  :root { --card-w: clamp(150px, 54vw, 210px); }
}

/* ===== THE ARC HAD TO OPEN UP ONCE THE CARDS BECAME PICTURES =====
 *
 * The side cards sat at ±62% of the card width, so each neighbour covered
 * nearly 40% of the card in front of it. With two lines of text per card that
 * read as depth. With a slide on every card it reads as a smear — the front
 * card's own caption competes with a neighbour's table showing through beside
 * it, and nothing looks like the subject.
 *
 * 86% leaves the front card fully clear with both neighbours legible at its
 * edges. Still a percentage, so it scales with the clamp rather than being a
 * pixel figure solved against one viewport — which is how the film and the
 * carousel got into trouble twice already.
 *
 * The Z push and the rotation are unchanged: they are what makes it an arc
 * rather than a row, and they were never the problem.
 */
/*
 * ===== 74%, AND THE NUMBER COMES FROM THE STAGE WIDTH =====
 *
 * Solved, not chosen. The stage is capped at 560px, so its half-width is 280.
 * A 320px card has a half-width of 160. The neighbour's centre therefore has
 * to sit within 280 - 160 = 120... which at 320px wide is 37%, and far too
 * tight to read as an arc.
 *
 * The resolution is that the stage is only 560 at its cap and the card only
 * 320 at its cap, and they clamp against different things — so the honest
 * figure is measured at the size that actually renders. At the desktop width
 * where the card is 206px, 74% puts the neighbour's centre at 152px and its
 * outer edge at 255 — inside the 280 half-stage, with the arrows clear.
 *
 * I TRIED 86% FIRST AND IT OVERFLOWED. The neighbours ran under the arrows and
 * the outermost pair hung off both edges of the stage. Measured in a browser,
 * not reasoned about.
 */
.reslink-slot {
  transform: translate(-50%, -50%) translateX(calc(var(--d, 0) * 74%))
    translateZ(calc(var(--d, 0) * var(--d, 0) * -60px))
    rotateY(calc(var(--d, 0) * -26deg));
}

/* Dimmer than the old 0.62. A picture at 62% still asks to be read; the point
   of a neighbour is to say "there is more this way". */
.reslink-slot[data-depth="1"] { opacity: 0.45; }

/*
 * ===== THE OUTERMOST PAIR IS HIDDEN AT EVERY SIZE NOW =====
 *
 * `.reslink-slot[data-depth="2"] { opacity: 0 }` already existed — inside
 * `@media (max-width: 768px), (max-height: 900px)`. So on a phone three cards
 * showed, and on a desktop five did, at 0.26 opacity.
 *
 * That was survivable with text cards. With five slides at 74% it is not: the
 * depth-2 pair sits at ±148% of the card width, which is outside the stage,
 * under the arrows and off both edges. It is the same fault as the page's
 * scroll mechanism — a rule that was right, gated behind a viewport size,
 * silently absent at the size the owner actually uses.
 *
 * Three cards is also the right number to show. The front one is the subject
 * and the two beside it say there is more in each direction; a fourth and
 * fifth at a quarter opacity add nothing a reader can use.
 */
.reslink-slot[data-depth="2"] { opacity: 0; }

/* ===== THE ARROWS ARE GONE, AND SO IS THEIR STYLING =====
 *
 * Removed from index.html on 17 September 2026. They did not work: the outer
 * pair of cards is painted at opacity 0 and was still hit-testable, sitting
 * directly over both buttons, so every click landed on an invisible card.
 * Replaced by hover on a desktop and swipe on a phone — both act on the cards
 * themselves, so there is no separate control left to be covered by one.
 *
 * The .reslink-arrow rules above are now inert. They are left in place rather
 * than deleted because nothing in this file is safe to remove by pattern
 * alone, and an unused selector costs a browser nothing.
 *
 * ===== A NEIGHBOUR MUST LOOK REACHABLE, OR NOBODY TRIES =====
 *
 * With the arrows gone there is no visible control at all. A cursor that
 * changes on the side cards is the cheapest possible affordance and the one
 * people already expect from a gallery.
 */
.reslink-slot[data-depth="1"] { cursor: pointer; }

/*
 * ===== THE HINT LINE WAS HERE AND IS GONE =====
 *
 * "Hover a page to bring it forward", with wording switched per input type by
 * media query. Added when the arrows were removed and there was suddenly no
 * visible control at all; removed the same day at the owner's request.
 *
 * WHAT IS LEFT AS THE AFFORDANCE: the pointer cursor on the side cards, above.
 * That is thinner than a sentence and it is the owner's call. If the gallery
 * ever reads as static again, this is the cheapest thing to put back — and it
 * belongs in CSS content per media query, not in script, so it is right on
 * first paint rather than after JavaScript runs.
 */

}

/* ===== THE CARD IS A BUTTON NOW, NOT A LINK =====
 *
 * It opens the slide large rather than navigating, at the owner's instruction
 * on 17 September 2026 — "no need to link them to the sign-up page". A button
 * arrives with a browser default background, border and font, none of which
 * the anchor had, so they have to be cleared or every card gains a grey
 * rectangle and system type.
 */
.reslink-card {
  appearance: none;
  -webkit-appearance: none;
  font: inherit;
  color: inherit;
  border: 1px solid rgba(201, 165, 74, 0.55);
  cursor: pointer;
  text-align: inherit;
}

/* ===== THE EXPANDED VIEW =====
 *
 * The front card is ~320px at its largest and the slides are pages of a
 * planner — tables, diagrams, labelled spirals. At that size they are texture,
 * not information, which is exactly what the owner reported: the words cannot
 * be read.
 *
 * FIXED, NOT ABSOLUTE. The page scrolls, so an absolutely positioned overlay
 * would sit wherever the gallery happens to be and the reader would have to
 * scroll to find it.
 */
.reslink-sheet {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  /* Only enough to keep the close button off the window edge; the picture
     itself is sized below. */
  padding: 20px;
  background: rgba(24, 20, 12, 0.88);
}

/* `hidden` is only display:none at the UA level, and `display: grid` above
   beats it on specificity. Without this the overlay is permanently open. */
.reslink-sheet[hidden] { display: none; }

/*
 * ===== IT FILLS THE WINDOW, WHICH IS WHAT WAS ASKED FOR =====
 *
 * This was `width: min(1100px, 92vw)` with the image capped at 80vh. On a
 * 786px window that produced a 723px picture — about 92% of the width and well
 * short of the height. The owner's words: "I told you to fill the ENTIRE
 * WINDOW".
 *
 * ===== THE 1100px CAP IS GONE, AND THAT IS THE REAL CHANGE =====
 *
 * The slides are 1100px wide as exported, so the old cap meant the overlay
 * could never be larger than the source file however big the window was. That
 * is the right instinct for a photograph, where upscaling shows; it is the
 * wrong one here, because these are diagrams and type — they stay legible
 * enlarged, and legibility is the entire point of the feature.
 *
 * ===== THE SUBTRACTION IS FOR THE CLOSE BUTTON, NOT FOR AIR =====
 *
 * calc(100vw - 88px) leaves 44px each side. The button is 36px and overhangs
 * the inner box by 8px, so anything less and it clips off the window edge on a
 * narrow screen — where it is also the only way out apart from Escape.
 */
.reslink-sheet-inner {
  position: relative;
  width: calc(100vw - 88px);
  max-height: calc(100vh - 88px);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/*
 * ===== NO LETTERBOX, BECAUSE THE BOX IS THE PICTURE =====
 *
 * `width: 100%` with `object-fit: contain` makes the ELEMENT the full width of
 * the overlay and then draws the image inside it — so whenever the window's
 * shape disagrees with the slide's, the difference is painted as bands of the
 * background colour at the sides. That is what a "faint line on the right
 * border" is made of.
 *
 * Letting the image size itself against two maximums instead means the element
 * always has exactly the picture's proportions. There is no spare space inside
 * it for a band to appear in, at any window shape, so the background below can
 * never show. It is kept only for the instant before the image decodes.
 *
 * ===== THE OTHER HALF OF THAT LINE WAS IN THE ARTWORK =====
 *
 * The cover carried a one-pixel dark frame down its left edge and a two-pixel
 * one down its right — baked into the file, luminance 106 against 238 for the
 * paper beside it, and present in the original PNG before any re-export here.
 * The four inside pages are clean. Cropped on re-export; see the note in
 * resources.js about replacing a slide.
 */
.reslink-sheet-img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: calc(100vh - 128px);
  margin: 0 auto;
  border-radius: 10px;
  background: #f2efe7;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}

.reslink-sheet-cap {
  margin: 0;
  text-align: center;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.86);
}

/*
 * OUTSIDE THE PICTURE, NOT ON IT. A close button painted over the top-right of
 * a slide covers part of what the reader opened it to see, and on a pale slide
 * a pale button disappears entirely.
 */
.reslink-sheet-close {
  position: absolute;
  top: -14px;
  right: -8px;
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  font-size: 22px;
  line-height: 1;
  color: #2b2418;
  background: #fffdf9;
  border: 1px solid rgba(201, 165, 74, 0.6);
  border-radius: 50%;
  cursor: pointer;
}

.reslink-sheet-close:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

@media (max-width: 560px) {
  .reslink-sheet { padding: 14px; }
  /* Less to subtract: the button is inset rather than overhanging here. */
  .reslink-sheet-inner {
    width: calc(100vw - 52px);
    max-height: calc(100vh - 52px);
  }
  .reslink-sheet-img { max-height: calc(100vh - 92px); }
  .reslink-sheet-close { top: -10px; right: -4px; }
}

/* ===== THE RING MUST NOT CATCH THE POINTER — THIS IS THE HOVER BUG =====
 *
 * The owner reported hover not working "when my desktop window is minimised".
 * Probed with elementFromPoint at each neighbour's own centre: the hit came
 * back as UL.reslink-ring every time, never the slot.
 *
 * ===== IT IS A 3D DEPTH PROBLEM, NOT A pointer-events ONE =====
 *
 * .reslink-stage sets `perspective: 900px` and .reslink-ring is
 * `position: absolute; inset: 0` with no transform — so the ring is a plane at
 * z = 0 spanning the whole stage. The neighbouring slots are pushed back by
 * `translateZ(-60px)`.
 *
 * Hit testing in a preserve-3d context picks the NEAREST surface. The ring, at
 * z = 0, is in front of every card except the front one — which is also at
 * z = 0 and later in paint order, which is exactly why the centre card always
 * worked and the neighbours never did.
 *
 * I FIRST BLAMED pointer-events, and that was wrong. paint() was setting
 * `pointer-events: none` on the non-front cards, which looked like the cause
 * and is a real thing to fix on its own — but turning it back to `auto`
 * changed nothing, because the ring was intercepting before the card was ever
 * consulted. Measured again rather than assumed, which is what found this.
 *
 * `pointer-events: none` on the ring makes it transparent to hit testing while
 * its children stay reachable. The ring paints nothing of its own, so it loses
 * nothing by never being a target.
 *
 * AND IT WAS NEVER ABOUT THE WINDOW SIZE. A wider window puts each neighbour's
 * centre over a different part of the arc, so it appeared to work sometimes.
 */
.reslink-ring { pointer-events: none; }
.reslink-slot { pointer-events: auto; }

/* The gold letters in "StrataGEM", matching the cover artwork. Scoped to the
   planner heading so it cannot leak into the rest of the page. */
.planner-title .pg { color: var(--gold-dark); }

/* "optional" beside a form label: present, and quieter than the label it
   qualifies. An unmarked field on a sign-up form reads as required. */
.join-optional {
  font-style: normal;
  font-weight: 400;
  font-size: 0.85em;
  opacity: 0.55;
}

/*
 * ===== "StrataGEM" IS ITALIC EVERYWHERE =====
 *
 * The name is a product title, so it is set in italic the way a book title is.
 * The rule lives here rather than in join.css because the name appears on the
 * home page, the sign-up page and the blog, and all three load this file —
 * one rule instead of three that drift.
 *
 * It is font-style only. The gold letters are a separate, narrower rule
 * (.planner-title .pg) so that the colour stays on the heading artwork and
 * does not follow the name into body text.
 */
.sg { font-style: italic; }

/* ==========================================================================
 * THE TRADEMARK MARK
 * ======================================================================== */

/*
 * ===== IT SITS OUTSIDE .sg, AND THAT IS DELIBERATE =====
 *
 * .sg makes "StrataGEM" italic. A trademark symbol is a legal mark, not part
 * of the word, so it is not italicised and not slanted along with the name.
 * Putting it inside the span would also have broken the test that asserts the
 * exact shape of the gold-lettered heading — which is the test noticing a real
 * typographic mistake rather than being in the way.
 *
 * ===== ONCE PER PAGE, ON THE MOST PROMINENT USE =====
 *
 * Not on every mention. The convention is to mark the first or most prominent
 * appearance; repeating it in running text reads as nervous rather than
 * protective, and it is the sort of thing that makes a young brand look like
 * it is trying too hard. One per page is the whole benefit.
 */
.tm {
  font-style: normal;
  font-weight: 400;
  font-size: 0.5em;
  /* Superscripts inflate the line box by default and would knock the heading
     off its baseline rhythm. vertical-align does the lift instead. */
  vertical-align: super;
  line-height: 0;
  letter-spacing: 0;
  margin-left: 0.08em;
  /* Quieter than the word it protects. It is a footnote, not a feature. */
  opacity: 0.65;
}

/* ==========================================================================
 * ADD TO HOME SCREEN
 * ======================================================================== */

/*
 * ===== READ THIS BEFORE TOUCHING display ON ANYTHING BELOW =====
 *
 * `[hidden] { display: none }` is a USER-AGENT style, and it has the
 * specificity of an attribute selector. Any class selector that sets `display`
 * beats it. The first version of this block said `.install-sheet { display:
 * flex }`, so the `hidden` attribute did nothing and an empty modal panel sat
 * on the home page from the moment it loaded.
 *
 * Nothing errored, and the tests passed — they checked the attribute was in
 * the markup, and it was. It was simply being ignored.
 *
 * So the rule is restated here, first, at class specificity. Every component
 * below is hidden by the attribute and nothing else.
 */
.install-btn[hidden],
.install-hint[hidden] {
  display: none !important;
}

/* ---------------------------------------------------------------- button -- */

/*
 * Top left, as asked. Fixed rather than in the flow: the corner it sits in
 * belongs to the hero, so it stays reachable while the page scrolls without
 * pushing the hero down by its own height.
 */
.install-btn {
  position: fixed;
  top: 14px;
  left: 14px;
  z-index: 40;

  display: inline-flex;
  align-items: center;
  gap: 7px;

  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 7px 13px 7px 10px;

  color: var(--dark, #241f16);
  background: rgba(255, 253, 249, 0.92);
  border: 1px solid rgba(201, 165, 74, 0.55);
  border-radius: 999px;
  cursor: pointer;

  /* The hero behind it is a photograph, so it needs its own edge to stay
     legible over whatever happens to be underneath. */
  box-shadow: 0 1px 6px rgba(36, 31, 22, 0.18);
  backdrop-filter: blur(6px);
}

.install-btn:hover { border-color: var(--gold, #c9a54a); }

.install-btn:focus-visible {
  outline: 2px solid var(--gold-dark, #a9822f);
  outline-offset: 2px;
}

.install-btn-icon {
  font-size: 1rem;
  line-height: 1;
  color: var(--gold-dark, #a9822f);
}

/*
 * ===== ON A PHONE THE WORDS GO, THE TARGET DOES NOT =====
 *
 * A 375px screen cannot give 150px of its top-left corner to a label. The text
 * is visually hidden and the button becomes a 40px circle — the reason to
 * shrink it is space, not to make it harder to hit.
 *
 * No aria-label is set: the text node stays in the DOM and is only visually
 * hidden, so a screen reader still announces "Add to Home Screen".
 */
@media (max-width: 560px) {
  .install-btn {
    top: 10px;
    left: 10px;
    padding: 0;
    width: 40px;
    height: 40px;
    justify-content: center;
    gap: 0;
  }

  .install-btn-text {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .install-btn-icon { font-size: 1.2rem; }
}

/* ------------------------------------------------------------------ hint -- */

/*
 * ===== A BAR, NOT A DIALOG =====
 *
 * It does not block the page, does not dim it, and does not have to be
 * dismissed before anything else can be done. It only ever appears after the
 * button is tapped.
 *
 * AT THE BOTTOM, because that is where Safari's Share button is on an iPhone
 * since iOS 15. A hint about a control should be next to the control.
 */
.install-hint {
  position: fixed;
  left: 12px;
  right: 12px;
  /* Clear of the home indicator on a notched iPhone; a plain 12px puts the
     bar under the swipe area, where taps land on the OS instead. */
  bottom: calc(12px + env(safe-area-inset-bottom, 0px));
  z-index: 55;

  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 12px 11px 14px;

  border-radius: 12px;
  background: rgba(36, 31, 22, 0.94);
  color: #fffdf9;
  box-shadow: 0 6px 24px rgba(36, 31, 22, 0.35);
}

.install-hint-text {
  margin: 0;
  flex: 1;
  font-size: 0.86rem;
  line-height: 1.35;
}

/*
 * ===== THE ARROW POINTS AT THE SHARE BUTTON =====
 *
 * Drawn in CSS rather than shipped as an image: one element, no request, and
 * it cannot fail to load and leave the bar looking like it lost something.
 */
.install-hint::after {
  content: "";
  position: absolute;
  bottom: -7px;
  left: 50%;
  width: 14px;
  height: 14px;
  transform: translateX(-50%) rotate(45deg);
  background: rgba(36, 31, 22, 0.94);
}

.install-hint-close {
  flex: 0 0 auto;
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 253, 249, 0.14);
  color: inherit;
  cursor: pointer;
}

.install-hint-close:focus-visible {
  outline: 2px solid var(--gold, #c9a54a);
  outline-offset: 2px;
}

/*
 * ===== NEVER INSIDE THE INSTALLED APP =====
 *
 * install.js already removes both on standalone. This is the backup: if the
 * script fails to load, the two things that cannot possibly be useful inside
 * an installed app still do not appear.
 */
@media (display-mode: standalone) {
  .install-btn,
  .install-hint { display: none !important; }
}


/* ==========================================================================
   THE BRAND BLOCK — LOGO AND WORDMARK

   ==========================================================================
   MOVED HERE OUT OF join.css, BECAUSE IT IS NOT A join.html THING
   ==========================================================================

   These rules lived in join.css, which only join.html and the blog pages load.
   The moment the survey and the employer-challenge pages needed the same mark,
   the options were to pull a whole page's stylesheet into them for three rules,
   or to write the rules a second time — and a second copy is how the spacing on
   these pages drifted apart in the first place.

   So they live in styles.css, which every page already loads.

   ===== WHY THE CLASSES ARE STILL CALLED .join-* =====

   Renaming them would mean touching join.html's markup and the blog generator
   at the same time as moving the file they live in. Two changes at once, and if
   anything broke it would not be obvious which caused it. The names are stale;
   the behaviour is shared. Worth renaming on a quiet day, on its own.

   .blog-masthead DID get renamed to .site-masthead, because it is now on four
   pages and a name that says "blog" would be actively misleading about where it
   is allowed to appear.
   ========================================================================== */

/*
 * 54px -> 150px, matching .hero-logo on the landing page.
 *
 * It was deliberately smaller — "a form, not an entrance" — and that was the
 * wrong call: someone arriving here has just clicked away from the hero, and
 * a mark that changes size between the two reads as a different site rather
 * than a quieter room of the same one. The source GIF renders at 480px, so
 * 150 stays crisp.
 */
.join-logo {
  width: 150px;
  height: auto;
  /*
   * ===== display:block, WHICH .hero-logo HAS AND THIS DID NOT =====
   *
   * An <img> is display:inline by default, so it sits on the text baseline and
   * the line box reserves DESCENDER SPACE beneath it — the classic few pixels of
   * mystery gap under an image that no margin explains.
   *
   * It happens not to bite today, because both places that use this class put it
   * in a flex container and flex items are blockified. That is a coincidence
   * holding two pages together: change .join-logo-wrap away from flex and the
   * join page silently gains several pixels that the home page does not, which
   * is exactly the class of drift this whole round has been about.
   *
   * The home page already declares it. Now both do.
   */
  display: block;
}

/*
 * THE WORDMARK, matched to .hero-content h1 on the landing page — same size
 * curve, same weight, same letter-spacing, same uppercase. These four values
 * are what make it read as the same mark; changing any one of them here would
 * reintroduce exactly the mismatch this fixes.
 *
 * Kept as its own class rather than merged into .join-title so the two pages
 * that share that class do not both become brand headings.
 */
.join-brand {
  font-size: clamp(18px, 3vw, 32px);
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
}

/* ==========================================================================
   THE SITE MASTHEAD

   Logo and wordmark above every blog page, index and post alike. Reuses
   .join-logo and .join-brand so it cannot drift from the same mark on
   join.html — a brand that is 150px on one page and 138px on another reads as
   a different site rather than a quieter room of the same one.
   ========================================================================== */

.site-masthead {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* A link, so it has to lose the underline and the link colour: it is a mark,
     not a sentence with a destination. */
  text-decoration: none;
  color: inherit;
  /*
   * ===== 24px OF AIR ABOVE, BECAUSE THIS SITS OUTSIDE .join-wrap =====
   *
   * MEASURED IN CHROME, not reasoned about. join.html's logo starts 24px below
   * the nav bar — that air is .join-wrap's own `padding: 24px 24px 80px`. This
   * masthead is a sibling of .join-wrap, not a child of it, so it inherited none
   * of that padding and started at 0: jammed hard against the nav.
   *
   * The logo-to-wordmark distance was already identical on all three pages by
   * then. This was the difference that was actually visible, and no amount of
   * looking at --brand-pull would have found it, because --brand-pull was
   * correct. The block was in the wrong place, not the wrong shape.
   *
   * 24px matches .join-wrap exactly rather than being chosen by eye, so the
   * masthead and the page body begin at the same distance from the nav.
   */
  margin: var(--brand-top) auto 8px;
  width: max-content;
}

/* The gap lives INSIDE the GIF — see the note on .join-logo-wrap above for why
   a margin could never close it. The SAME value every other page uses, read
   from --brand-pull rather than copied, because a fourth literal is a fourth
   thing to remember. */
.site-masthead-name {
  margin-top: var(--brand-pull);
}

.site-masthead:hover .site-masthead-name,
.site-masthead:focus-visible .site-masthead-name {
  color: var(--gold-dark, #a9822f);
}

.site-masthead:focus-visible {
  outline: 2px solid var(--gold-dark, #a9822f);
  outline-offset: 6px;
  border-radius: 4px;
}

/* ===== THE INDEX'S OWN HEADING NOW HAS THE WORDMARK ABOVE IT =====
   "Blog" sat with 28px under it and nothing over it. With a mark above, that
   spacing reads as a gap rather than as air. */
/*
 * ===== THE WRAPPER THAT FOLLOWS THE MASTHEAD =====
 *
 * .challenge-wrap pads 56px on top — right when it was the first thing under
 * the nav, and a hole when a masthead sits above it. Collapsed to --brand-top so
 * the page title sits under the wordmark the same way the blog's does.
 *
 * SCOPED WITH +, so the survey and challenge pages keep their 56px if the
 * masthead is ever removed, rather than jumping to the top of the window.
 */
.site-masthead + .challenge-wrap {
  padding-top: var(--brand-top);
}

.site-masthead + .join-wrap .join-title:first-child {
  margin-top: 6px;
}
