/* ==========================================================================
   EVENT CALENDAR LOCKDOWN
   Page-specific layer for /v6-2/event-calendar-lockdown/.

   Loads THIRD, after style.css (the design system) and sprint.css (the
   portfolio, lightbox and split-hero components this page reuses). Everything
   here is either a device that only exists on this page, or a deliberate
   override of the two files above.

   House rules this file follows, taken from the two existing pages:
     - colour only ever comes from the token set; no raw hex except where a
       token cannot reach (there are none in this file)
     - 1px hairlines in var(--line), 2px radius, nothing rounder
     - mono for labels and metadata, uppercase, wide tracking
     - clamp() for anything that scales; no fixed desktop-only sizing
     - every animation has a prefers-reduced-motion escape

   SPECIFICITY TRAP, inherited from the rest of the system: `:root .mono` is
   (0,2,0), so any rule that recolours a .mono element needs three classes to
   win. Several rules below carry a redundant-looking ancestor for exactly
   that reason. Do not "tidy" them.
   ========================================================================== */

/* ---------------- 01 hero: the year board ----------------
   The Sprint page's hero shows photographs because it sells one project on the
   strength of the craft. This page sells a YEAR, so the hero shows a year: a
   twelve-month strip with the event dates already marked. The marks live in
   the HTML so the board still reads with JavaScript off; lockdown.js only
   staggers them in. */

.yearboard { margin: 0; display: grid; gap: 0.7rem; align-content: start; }

.yb {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink) 2.5%, var(--paper));
  padding: clamp(1rem, 2vw, 1.5rem);
  display: grid;
  gap: clamp(0.9rem, 1.8vw, 1.3rem);
}
.yb__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem 1rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--line);
}
/* three classes to beat `:root .mono` */
.yb .yb__head .yb__tally { color: var(--accent-label); }

.yb__months {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: clamp(2px, 0.5vw, 6px);
  align-items: end;
}
.yb__month { display: grid; gap: 0.45rem; justify-items: center; min-width: 0; }
/* The column is a fixed-height track so every month shares one baseline and
   the marks stack up from it. Without the fixed height the row would jump
   about as months carry different numbers of events. */
.yb__col {
  width: 100%;
  height: clamp(84px, 15vw, 132px);
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-start;
  gap: 3px;
  padding: 3px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--ink) 3%, transparent);
}
.yb__mark {
  display: block;
  width: 100%;
  height: clamp(14px, 2.4vw, 20px);
  border-radius: 1px;
  flex: none;
  border: 1px solid var(--accent-label);
}
.yb__mark--photo { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.yb__mark--video { background: color-mix(in srgb, var(--accent) 55%, transparent); }
.yb__mark--both  { background: var(--accent); }
.yb__mark--live  { background: var(--ink); border-color: var(--ink); }
/* three classes, same reason as above */
.yb .yb__months .yb__lab { font-size: calc(var(--mono-size) - 1px); }

.yb__key {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.15rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
}
.yb__key li { display: inline-flex; align-items: center; gap: 0.42rem; }
.yb__key .yb__mark { width: 14px; height: 10px; }

/* the stagger. .yb-in is added by lockdown.js once the board is in view */
@media (prefers-reduced-motion: no-preference) {
  .yb[data-yearboard] .yb__months .yb__mark {
    opacity: 0;
    transform: scaleY(0.2);
    transform-origin: bottom;
  }
  .yb[data-yearboard].yb-in .yb__months .yb__mark {
    opacity: 1;
    transform: none;
    transition: opacity 340ms ease, transform 420ms cubic-bezier(0.2, 0.8, 0.3, 1);
    transition-delay: var(--d, 0ms);
  }
}

@media (max-width: 900px) {
  /* The board keeps all twelve months on a phone rather than scrolling: the
     whole point is seeing a year at once, and a strip you have to swipe is not
     a year, it is a list. The columns simply get shorter. */
  .yb__col { height: clamp(56px, 17vw, 96px); }
  .yb__mark { height: clamp(9px, 2.6vw, 16px); }
  .yb__key { gap: 0.4rem 0.9rem; }
}
@media (max-width: 620px) {
  /* Twelve three-letter labels do not fit a phone: they run together into one
     unreadable string (JANFEBMARAPR...). Swap to initials, which is how a
     calendar spine is normally abbreviated anyway. font-size:0 hides the text
     node while leaving ::before to render, so the full month name stays in the
     markup for assistive tech and for anyone reading the source. */
  .yb .yb__months .yb__lab { font-size: 0; letter-spacing: 0; }
  .yb .yb__months .yb__lab::before {
    content: attr(data-short);
    font-size: calc(var(--mono-size) - 1px);
    letter-spacing: 0.04em;
  }
}

/* ---------------- 03 problem: the loop against the track ----------------
   The whole argument is carried by two shapes. The left column is a closed
   loop that returns to its own start, visually crowded on purpose. The right
   column is a straight track that ends. A reader who only glances still gets
   the point, which is the job of this section. */

.vs {
  margin-top: clamp(2.4rem, 5vw, 4rem);
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.2rem, 2.6vw, 2.2rem);
  /* stretch, not start: the two panels must be the same height. The left one
     is always taller because it carries seven steps to the right one's four,
     and letting each size to its own content made the right panel look
     unfinished rather than shorter-on-purpose. */
  align-items: stretch;
}
.vs__side {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.1rem, 2.2vw, 1.7rem);
  display: grid;
  /* head, steps, then the closing line takes the remaining height and pins
     itself to the bottom of it. That puts "repeat for every event" and
     "agreed once, used all year" on the same baseline across both panels,
     which is the whole comparison stated one more time. The steps keep their
     natural spacing: stretching them to fill would space the four out to match
     the seven and quietly undo the point being made. */
  grid-template-rows: auto auto 1fr;
  gap: 1.1rem;
}
.vs__side--old { background: color-mix(in srgb, var(--ink) 3%, transparent); }
.vs__side--new {
  background: color-mix(in srgb, var(--accent) 3%, var(--paper));
  box-shadow: 0 0 0 1px var(--accent);
}
.vs__head { display: grid; gap: 0.5rem; }
.vs .vs__head .vs__tag { color: var(--dim); }
.vs .vs__head .vs__tag--on { color: var(--accent-label); }
.vs__sum {
  font-size: clamp(1.05rem, 1.7vw, 1.35rem);
  font-weight: 500;
  letter-spacing: -0.015em;
  line-height: 1.25;
}

.loopsteps, .tracksteps { list-style: none; display: grid; gap: 0; }
.loopsteps li, .tracksteps li {
  display: grid;
  grid-template-columns: 2.4rem 1fr;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.62rem 0;
  border-bottom: 1px dashed var(--line);
  font-size: 0.97rem;
}
.loopsteps li:last-child, .tracksteps li:last-child { border-bottom: 0; }
/* the loop's last step points back at its own first one */
.loopsteps .loopsteps__repeat { color: var(--accent-label); font-weight: 500; }
.tracksteps li { position: relative; }
.tracksteps li::after {
  content: "";
  position: absolute;
  left: 0.72rem;
  top: 1.55rem;
  bottom: -0.62rem;
  width: 1px;
  background: color-mix(in srgb, var(--accent) 45%, transparent);
}
.tracksteps li:last-child::after { display: none; }
.vs__foot { padding-top: 0.2rem; align-self: end; }
.vs .vs__side--new .vs__foot { color: var(--accent-label); }

@media (max-width: 760px) {
  .vs { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------- 04 how it works ---------------- */

.howsteps {
  list-style: none;
  margin-top: clamp(2.2rem, 4.5vw, 3.4rem);
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.6rem);
}
.howstep {
  display: grid;
  gap: 0.55rem;
  align-content: start;
  padding-top: 0.9rem;
  border-top: 2px solid var(--ink);
}
.howsteps .howstep .howstep__n { color: var(--accent-label); }
.howstep h3 {
  font-size: clamp(1rem, 1.35vw, 1.15rem);
  font-weight: 600;
  line-height: 1.25;
  letter-spacing: -0.015em;
}
.howstep p { font-size: 0.95rem; color: var(--dim); line-height: 1.5; }
/* step five is the one people misread, so it is marked */
.howsteps .howstep:last-child { border-top-color: var(--accent); }

@media (max-width: 1080px) { .howsteps { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 700px)  { .howsteps { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 460px)  { .howsteps { grid-template-columns: minmax(0, 1fr); } }

/* ---------------- the pool graphic ----------------
   Load-bearing. "Bonuses sit on top of the pool" is the single most
   misunderstood part of this offer, so it is expressed as physical separation:
   a detached row, a labelled rule, then the bar. Anyone who redesigns this
   must keep that separation, or the sentence stops being true on screen. */

.pool {
  margin: clamp(2.4rem, 5vw, 4rem) 0 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  padding: clamp(1.2rem, 2.6vw, 2rem);
  display: grid;
  gap: clamp(1rem, 2vw, 1.4rem);
}
.pool__bonus { display: grid; gap: 0.7rem; }
.pool .pool__bonus .pool__bonus__tag { color: var(--accent-label); }
.pool__chips { list-style: none; display: flex; flex-wrap: wrap; gap: 0.5rem; }
/* flex-grow, not a fixed width: whichever chips land on a line together
   share it evenly, so a chip that wraps onto a row by itself (no fixed
   pairing - which two end up together depends on viewport width) stretches
   to fill the row instead of leaving dead space beside it, and a pair
   fills the row between them rather than sitting at their own content
   width with a gap. Self-adjusting at any width, so it does not need to
   know which chips are actually going to end up alone. */
.poolchip {
  flex: 1 1 auto;
  min-width: 9rem;
  text-align: center;
  border: 1px dashed var(--accent-label);
  border-radius: var(--radius);
  padding: 0.34rem 0.62rem;
  font-size: 0.88rem;
  color: var(--accent-label);
  background: color-mix(in srgb, var(--accent) 4%, transparent);
}

/* the labelled rule that does the separating */
.pool__rule {
  position: relative;
  height: 1px;
  background: var(--line);
  margin: clamp(0.4rem, 1vw, 0.8rem) 0;
}
.pool .pool__rule span {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background: var(--paper);
  padding: 0 0.7rem;
  white-space: nowrap;
  font-size: calc(var(--mono-size) - 1px);
  color: var(--dim);
}

.pool__meter { display: grid; gap: 0.7rem; }
.pool__bar {
  display: flex;
  width: 100%;
  height: clamp(56px, 9vw, 76px);
  border: 1px solid var(--ink);
  border-radius: var(--radius);
  overflow: hidden;
}
.pool__seg {
  display: flex;
  align-items: flex-end;
  min-width: 0;
  width: var(--w);
  padding: 0.35rem;
  border-right: 1px solid var(--paper);
  overflow: hidden;
}
.pool__seg:last-child { border-right: 0; }
/* CONTRAST, not decoration. These labels are ~9px uppercase mono, which is
   small text and needs 4.5:1. White on the true accent (#FD4700) is only
   3.4:1 and fails, which is the same trap the Sprint page hit on its featured
   flag. The house answer there was to darken the ground; here the simpler
   answer is to keep every segment light enough for INK text, so all four
   labels share one colour instead of some being white and some dark.
   Pure accent against ink measures 5.8:1, and the two lighter mixes are
   better still. Do not darken these without re-checking the labels. */
.pool__seg--a { background: var(--accent); }
.pool__seg--b { background: color-mix(in srgb, var(--accent) 70%, var(--paper)); }
.pool__seg--c { background: color-mix(in srgb, var(--accent) 42%, var(--paper)); }
.pool__seg--open {
  background: repeating-linear-gradient(
    -45deg,
    color-mix(in srgb, var(--ink) 6%, transparent) 0 6px,
    transparent 6px 12px
  );
}
.pool__seg__lab {
  font-family: var(--font-mono);
  font-size: calc(var(--mono-size) - 2px);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  line-height: 1.1;
  color: var(--ink);
  text-wrap: balance;
}
.pool__seg--open .pool__seg__lab { color: var(--dim); }

/* fill on arrival */
@media (prefers-reduced-motion: no-preference) {
  .pool[data-pool] .pool__seg { width: 0; }
  .pool[data-pool].pool-in .pool__seg {
    width: var(--w);
    transition: width 900ms cubic-bezier(0.22, 1, 0.36, 1);
    transition-delay: var(--d, 0ms);
  }
}

@media (max-width: 700px) {
  /* Segment labels stop fitting long before the bar does. Drop them and keep
     the proportions, which is the part that carries the meaning. The section
     still explains itself in the steps above and in the aria-label. */
  .pool__seg__lab { display: none; }
  .pool__bar { height: 44px; }
  .pool .pool__rule span { font-size: calc(var(--mono-size) - 2px); }
}

/* ---------------- 05 partnership tiers ----------------
   Written to read as partnerships rather than as a price list. The name and
   who it suits come first; the pool figure arrives underneath as supporting
   evidence. That ordering is the brief and it is also just true: nobody
   chooses one of these on price alone.

   Subgrid keeps the pool figure, the list and the CTA on shared baselines
   across all three cards. NOTE the row-gap override: a subgrid inherits the
   parent's gutters, so without its own row-gap the card's internal spacing and
   the gap BETWEEN cards are the same number, and the featured card's flag
   collides with the card above it when the grid wraps. That bug was found and
   fixed on the Sprint page; do not remove the override. */

.tiers {
  margin-top: clamp(2.4rem, 5vw, 3.6rem);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.1rem, 2.2vw, 1.8rem);
  row-gap: clamp(2.2rem, 3.4vw, 3rem);
}
.tier {
  position: relative;
  grid-row: span 5;
  display: grid;
  grid-template-rows: subgrid;
  row-gap: 1.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.2rem, 2.4vw, 1.8rem);
  background: var(--paper);
}
.tier--featured {
  box-shadow: 0 0 0 1px var(--accent);
  background: color-mix(in srgb, var(--accent) 2%, var(--paper));
}
/* THREE classes. The flag is a .mono element and `:root .mono` is (0,2,0), so
   a one-class rule loses the colour fight and the label renders in --dim
   orange on an orange ground: invisible, and measured at 1.0:1 before this was
   caught. This is the trap named at the top of this file. */
.tiers .tier .tier__flag {
  position: absolute;
  top: 0;
  right: clamp(1.2rem, 2.4vw, 1.8rem);
  transform: translateY(-50%);
  /* --accent-label, not --accent, for the same reason the Sprint page's
     featured flag uses it: white on #FD4700 is 3.4:1 and fails small-text AA,
     white on #B93400 is 6.6:1. The card keeps the true accent on its ring. */
  background: var(--accent-label);
  color: #fff;
  border-radius: var(--radius);
  padding: 0.24rem 0.6rem;
  font-size: calc(var(--mono-size) - 1px);
  letter-spacing: 0.06em;
}
.tier__head { display: grid; gap: 0.45rem; align-content: start; }
.tier__name {
  font-family: var(--font-disp);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(1.35rem, 2.1vw, 1.75rem);
  line-height: 1.02;
  letter-spacing: -0.015em;
}
.tier__who { color: var(--dim); font-size: 0.95rem; line-height: 1.45; }

.tier__pool {
  display: grid;
  gap: 0.25rem;
  align-content: start;
  padding: 0.9rem 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.tier__pool__fig {
  font-size: clamp(1.9rem, 3.4vw, 2.6rem);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1;
}
.tier .tier__pool .tier__pool__tag { color: var(--dim); }
.tier .tier__pool .tier__pool__pay { color: var(--dim); font-size: calc(var(--mono-size) - 1px); }

.tier__list { list-style: none; display: grid; gap: 0.5rem; align-content: start; }
.tier__list li {
  position: relative;
  padding-left: 1.15rem;
  font-size: 0.95rem;
  line-height: 1.45;
}
.tier__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.8;
}

/* Added 2026-09-07. Each card used to run one flat list mixing rate/terms
   items ("Better annual production rates") with free-bonus items ("2 headshot
   refreshes") with nothing to tell them apart - a buyer had to guess which
   was which. Now wrapped in ONE .tier__benefits div, so the card's subgrid row
   count (`.tier { grid-row: span 5 }`) does not need to change: this whole
   block is still a single grid child, it just stacks two lists and a label
   inside itself. */
.tier__benefits { display: grid; gap: 0.6rem; align-content: start; }
.tier__list--rate { margin: 0; }
/* The label that removes the guessing. Same visual weight as .tier__bonusval
   below it (accent-label colour, dashed rule above), so the two "this is free"
   moments in a card read as one family. */
.tiers .tier .tier__bonuslabel {
  color: var(--accent-label);
  border-top: 1px dashed var(--line);
  padding-top: 0.6rem;
  margin-top: 0.1rem;
}
.tier__list--bonus { margin: 0; }
.tier__list--bonus li::before { background: var(--accent-label); opacity: 1; }

/* Stated as a plain figure with the word "value". Never struck through, never
   presented as a discount off a former price: the page has no fake anchoring
   anywhere and must not gain any. */
.tier .tier__bonusval {
  align-self: end;
  color: var(--accent-label);
  border-top: 1px dashed var(--line);
  padding-top: 0.8rem;
}
.tier__cta { align-self: end; justify-content: space-between; text-align: left; }

.tiernote {
  margin-top: clamp(1.8rem, 3.4vw, 2.6rem);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.8rem 2rem;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  padding: clamp(1rem, 2vw, 1.4rem);
}
.tiernote p { color: var(--dim); max-width: 62ch; }

@media (max-width: 1000px) {
  .tiers { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 680px) {
  .tiers { grid-template-columns: minmax(0, 1fr); }
  /* Subgrid across one column just stretches the rows apart, so the cards go
     back to being ordinary flow containers on a phone. */
  .tier { grid-row: auto; display: block; }
  .tier > * + * { margin-top: 1.1rem; }
  /* Full width to match the card, all three ("Start with Essential",
     "Choose Partner", "Choose Priority"): at their own content width they
     left visible dead space beside them on a phone. justify-content is
     already space-between on the base rule, so stretching the button just
     means the label and the arrow icon actually spread to the two edges. */
  .tier__cta { width: 100%; }
}

/* ---------------- 06 what the pool covers ---------------- */

.uses {
  list-style: none;
  margin-top: clamp(2.2rem, 4.5vw, 3.4rem);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
/* A hairline grid rather than eleven separate cards: it reads as one menu of
   options, which is the point being made about flexibility. */
.use {
  background: var(--paper);
  padding: clamp(1rem, 1.9vw, 1.4rem);
  display: grid;
  gap: 0.4rem;
  align-content: start;
  transition: background 220ms ease;
}
.use:hover { background: color-mix(in srgb, var(--accent) 4%, var(--paper)); }
.uses .use .use__n { color: var(--accent-label); }
.use h3 { font-size: 1rem; font-weight: 600; letter-spacing: -0.015em; line-height: 1.25; }
.use p { font-size: 0.9rem; color: var(--dim); line-height: 1.45; }
.use--open { background: color-mix(in srgb, var(--ink) 4%, var(--paper)); }
@media (prefers-reduced-motion: reduce) { .use { transition: none; } }

@media (max-width: 980px) { .uses { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 720px) { .uses { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 440px) { .uses { grid-template-columns: minmax(0, 1fr); } }

/* ---------------- 07 what a year looks like ---------------- */

.yearview {
  margin-top: clamp(2.2rem, 4.5vw, 3.2rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  /* overflow:hidden used to live here, clipping .yeartabs to the card's own
     rounded corners. Removed 2026-09-08: position:sticky on a descendant is
     computed relative to the nearest ancestor with overflow other than
     visible, and having one here - even though it never actually needed to
     scroll, only to clip - silently stopped .yeartabs from sticking at all
     (confirmed directly: removing this one declaration was the entire fix).
     The corners are rounded a different way now instead - see :first-child/
     :last-child on .yeartab below - so nothing here needs to clip anything
     any more. */
}
.yeartabs {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  border-bottom: 1px solid var(--line);
}
.yeartab {
  appearance: none;
  border: 0;
  border-right: 1px solid var(--line);
  background: color-mix(in srgb, var(--ink) 4%, transparent);
  color: var(--dim);
  font-family: var(--font-body);
  font-size: clamp(0.95rem, 1.5vw, 1.1rem);
  font-weight: 600;
  letter-spacing: -0.015em;
  padding: clamp(0.85rem, 1.8vw, 1.15rem) clamp(0.6rem, 1.4vw, 1.1rem);
  cursor: pointer;
  display: grid;
  gap: 0.2rem;
  justify-items: center;
  transition: background 200ms ease, color 200ms ease;
}
.yeartab:last-child { border-right: 0; }
/* Corner-rounding moved here from .yearview's own overflow:hidden (removed
   above) - the tab row is the only child with a background that ever
   reached the card's own top corners, so rounding it directly (each button
   rounds its own box; no clipping needed) reproduces the old look exactly. */
.yeartab:first-child { border-top-left-radius: var(--radius); }
.yeartab:last-child { border-top-right-radius: var(--radius); }
.yeartab[aria-selected="true"] {
  background: var(--paper);
  color: var(--ink);
  box-shadow: inset 0 3px 0 0 var(--accent);
}
.yeartabs .yeartab .mono { font-size: calc(var(--mono-size) - 1px); }
.yeartabs .yeartab[aria-selected="true"] .mono { color: var(--accent-label); }
@media (prefers-reduced-motion: reduce) { .yeartab { transition: none; } }

.yearpanel { padding: clamp(1.2rem, 2.6vw, 2rem); display: grid; gap: clamp(1.2rem, 2.4vw, 1.8rem); }
/* MUST come after the display rule above. `[hidden] { display: none }` lives in
   the browser's own stylesheet, which any author rule outranks, so a bare
   `display: grid` on a panel silently defeats the hidden attribute: the tab
   script sets .hidden correctly and all three panels keep rendering anyway.
   Trevor caught this on the live page. Any future rule in this file that sets
   display on an element which is ever toggled with [hidden] needs the same
   companion, or the toggle does nothing. */
.yearpanel[hidden] { display: none; }
.yearpanel:focus-visible { outline-offset: -3px; }
.yearpanel__top {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: clamp(1rem, 2.4vw, 2rem);
  align-items: start;
}
.yearpanel__lede { color: var(--dim); font-size: var(--body); line-height: 1.5; max-width: 62ch; }
.yearstats { list-style: none; display: grid; gap: 0.8rem; justify-items: start; }
.yearstats li { display: grid; gap: 0.1rem; }
.yearstat__fig {
  font-family: var(--font-disp);
  font-size: clamp(2.2rem, 4.4vw, 3.4rem);
  line-height: 0.9;
  letter-spacing: -0.02em;
  color: var(--accent-label);
}

/* the event chips: one chip is one date on the calendar */
.evgrid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 0.55rem;
}
.ev {
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.6rem 0.7rem;
  display: grid;
  gap: 0.2rem;
  background: color-mix(in srgb, var(--ink) 2%, var(--paper));
  min-width: 0;
}
.evgrid .ev .ev__k { color: var(--dim); font-size: calc(var(--mono-size) - 2px); }
.ev__t { font-size: 0.9rem; font-weight: 500; line-height: 1.3; letter-spacing: -0.01em; }
.ev--photo { border-left-color: color-mix(in srgb, var(--accent) 40%, var(--paper)); }
.ev--video { border-left-color: color-mix(in srgb, var(--accent) 70%, var(--paper)); }
.ev--both  { border-left-color: var(--accent); }
.ev--live  { border-left-color: var(--ink); background: color-mix(in srgb, var(--ink) 5%, var(--paper)); }

.yearbonus {
  border-top: 1px dashed var(--line);
  padding-top: clamp(1rem, 2vw, 1.4rem);
  display: grid;
  gap: 0.7rem;
}
/* Made deliberately louder than the same tag elsewhere (the pool graphic,
   the tier cards) at Trevor's request 2026-09-07: this is the one place on the
   page where a reader has just scanned a dense grid of paid event dates, so
   the free-bonus line needs to stop the eye rather than read as more
   metadata. Bumped off the small .mono size, bolded, and given the same
   dot-bullet device .slabel uses elsewhere, scaled up. */
.yearview .yearbonus .yearbonus__tag {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--accent-label);
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}
.yearview .yearbonus .yearbonus__tag::before {
  content: "";
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
.yearbonus__list { list-style: none; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.yearbonus__list li {
  border: 1px dashed var(--accent-label);
  border-radius: var(--radius);
  padding: 0.32rem 0.6rem;
  font-size: 0.88rem;
  color: var(--accent-label);
  background: color-mix(in srgb, var(--accent) 4%, transparent);
}

@media (max-width: 820px) {
  .yearpanel__top { grid-template-columns: minmax(0, 1fr); }
  .yearstats { grid-auto-flow: column; justify-content: start; gap: 2rem; }
}
@media (max-width: 620px) {
  /* Redesigned 2026-09-08: this used to stack the three tabs into one
     column ("three tabs of full words do not fit a phone"), on the theory
     that a squeezed horizontal row would be hard to read and hit. Trevor
     asked for horizontal back on a phone too - at "Essential"/"Partner"/
     "Priority" plus a short price line, three columns turns out to fit
     fine at ordinary phone widths, so the base (desktop) grid is left
     alone here rather than overridden.

     Floating, per the same request: sticky, not fixed, so it only pins
     while .yearview (its own container) is actually scrolling past - once
     the card ends it releases and scrolls away normally, same mechanism
     the proof band uses higher up this page. An explicit background is
     required: without one, .yearpanel's own content would show through
     underneath the tabs while pinned, since sticky does not implicitly
     opaque anything. */
  .yeartabs {
    position: sticky;
    top: var(--header-h);
    z-index: 2;
    background: var(--paper);
  }
  .evgrid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); }

  /* flex:1 1 auto (same fix as .poolchip above) so a chip left alone on its
     row stretches to the same right edge every other row ends at, and a
     pair splits the row evenly - instead of each chip sizing to its own
     text and leaving a ragged edge that differs tab to tab. Trevor asked
     for this on mobile only 2026-09-08 - the desktop grid above is left at
     its original content-sized width. */
  .yearbonus__list li {
    flex: 1 1 auto;
    min-width: 9rem;
    text-align: center;
  }
}

/* ---------------- 08 the bonuses (on the dark surface) ----------------
   surface--dark flips the token set, so these cards need no colour of their
   own. It also remaps --accent-label to the bright --accent, because the
   darkened label orange only reaches about 3.3:1 on near-black and fails AA.
   That remap lives in style.css; do not override it here. */

.bongrid {
  margin-top: clamp(2.2rem, 4.5vw, 3.4rem);
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.5rem);
}
.boncard {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1.1rem, 2.2vw, 1.6rem);
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  background: color-mix(in srgb, var(--ink) 4%, transparent);
}
/* The first two carry the most explanation, so they take three columns each
   and the remaining three take two. A plain equal grid left the long cards
   cramped and the short ones half empty. */
.boncard:nth-child(1), .boncard:nth-child(2) { grid-column: span 3; }
.boncard:nth-child(3), .boncard:nth-child(4), .boncard:nth-child(5) { grid-column: span 2; }
.boncard h3 {
  font-size: clamp(1.1rem, 1.7vw, 1.35rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.boncard p { color: var(--dim); font-size: 0.97rem; line-height: 1.5; }
.boncard__uses { list-style: none; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.boncard__uses li {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.26rem 0.55rem;
  font-size: 0.85rem;
  color: var(--dim);
}
.bongrid .boncard .boncard__spec {
  color: var(--accent-label);
  border-top: 1px solid var(--line);
  padding-top: 0.7rem;
  margin-top: 0.1rem;
}

/* Per-card tier availability, added 2026-09-09 at Trevor's request: which
   partnerships include this bonus, and how many times a year, as a chip
   row instead of a sentence - matches the "Free partnership bonuses
   include:" dashed-chip pattern used in the year board higher up the page
   (.yearbonus__list), but kept as its own class since that one is a
   generic, unscoped selector shared with a different section - duplicating
   it here rather than reusing it directly avoids coupling the two. Colours
   are plain var(--accent-label)/var(--accent) so this needs no dark-mode
   styling of its own: surface--dark already remaps both for this section
   (see the .boncard comment above). */
.boncard__tiers {
  border-top: 1px solid var(--line);
  padding-top: 0.7rem;
  margin-top: auto;
  display: grid;
  gap: 0.5rem;
}
.boncard__tiers__tag {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--accent-label);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: 700;
}
.boncard__tiers__tag::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  flex: none;
}
.boncard__tierlist { list-style: none; display: flex; flex-wrap: wrap; gap: 0.5rem; }
.boncard__tierlist li {
  border: 1px dashed var(--accent-label);
  border-radius: var(--radius);
  padding: 0.3rem 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  color: var(--accent-label);
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}

@media (max-width: 1000px) {
  .bongrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .boncard:nth-child(n) { grid-column: span 1; }
}
@media (max-width: 620px) {
  .bongrid { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------- 09 the event cycle rail ----------------
   A rail, not a list of four cards: the claim is that the partnership runs
   across a continuous cycle, and a connected horizontal run is that claim
   drawn. The connecting line is a pseudo-element on the list so it sits behind
   the stations and cannot be knocked out of alignment by their content. */

.rail {
  list-style: none;
  position: relative;
  margin-top: clamp(2.4rem, 5vw, 3.6rem);
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(1rem, 2vw, 1.6rem);
}
.rail::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 7px;
  height: 1px;
  background: repeating-linear-gradient(
    90deg,
    var(--line) 0 6px,
    transparent 6px 12px
  );
}
.rail__stop {
  position: relative;
  padding-top: 1.7rem;
  display: grid;
  gap: 0.5rem;
  align-content: start;
}
.rail__stop::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 1px solid var(--accent-label);
  background: var(--paper);
}
.rail__stop--peak::before { background: var(--accent); border-color: var(--accent); }
.rail__stop--between::before { background: var(--ink); border-color: var(--ink); }
.rail .rail__stop .rail__when { color: var(--accent-label); }
.rail__stop h3 {
  font-size: clamp(1rem, 1.5vw, 1.2rem);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.25;
}
.rail__stop ul { list-style: none; display: grid; gap: 0.3rem; }
.rail__stop li {
  position: relative;
  padding-left: 0.95rem;
  font-size: 0.93rem;
  color: var(--dim);
  line-height: 1.4;
}
.rail__stop li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 4px;
  height: 1px;
  background: currentColor;
  opacity: 0.7;
}

@media (max-width: 860px) {
  /* Below the fold of a phone the rail turns vertical, and the connector turns
     with it: same figure, rotated, rather than a different component. */
  .rail { grid-template-columns: minmax(0, 1fr); gap: 1.6rem; }
  .rail::before { left: 7px; right: auto; top: 0; bottom: 0; width: 1px; height: auto;
    background: repeating-linear-gradient(180deg, var(--line) 0 6px, transparent 6px 12px); }
  .rail__stop { padding-top: 0; padding-left: 2.1rem; }
  .rail__stop::before { top: 3px; }
}

/* ---------------- 11 why ---------------- */

.whys {
  list-style: none;
  margin-top: clamp(2.2rem, 4.5vw, 3.4rem);
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(1.2rem, 2.4vw, 2rem);
}
.why { display: grid; gap: 0.4rem; align-content: start; padding-top: 0.9rem; border-top: 1px solid var(--ink); }
.why h3 { font-size: clamp(1.05rem, 1.6vw, 1.25rem); font-weight: 600; letter-spacing: -0.015em; }
.why p { color: var(--dim); font-size: 0.95rem; line-height: 1.5; }
@media (max-width: 880px) { .whys { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .whys { grid-template-columns: minmax(0, 1fr); } }

/* ---------------- 13 if the calendar grows ---------------- */

.growbox {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(1.4rem, 3vw, 3rem);
  align-items: start;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 3%, var(--paper));
  padding: clamp(1.4rem, 3vw, 2.4rem);
}
.growbox__copy { display: grid; gap: 0.7rem; align-content: start; }
.growbox__copy h2 {
  font-size: clamp(1.6rem, 3vw, 2.4rem);
  font-weight: 500;
  line-height: 1.12;
  letter-spacing: -0.022em;
  text-wrap: balance;
}
.growbox__copy p { color: var(--dim); line-height: 1.5; }
.growlist { list-style: none; display: grid; gap: 0.9rem; align-content: start; }
.growlist li { display: grid; grid-template-columns: 2.2rem 1fr; gap: 0.5rem; align-items: baseline; }
.growbox .growlist .mono { color: var(--accent-label); }
.growlist p { font-size: 0.97rem; line-height: 1.5; }
@media (max-width: 780px) { .growbox { grid-template-columns: minmax(0, 1fr); } }

/* ---------------- 14 FAQ extras ---------------- */

.faq .faq__a .faq__soon { color: var(--accent-label); margin-top: 0.5rem; }

.rollover {
  margin: 0.9rem 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.9rem 1rem;
  background: color-mix(in srgb, var(--ink) 3%, transparent);
}
.faq .rollover .rollover__tag { color: var(--dim); }
.rollover__rows { list-style: none; display: grid; gap: 0.35rem; margin-top: 0.6rem; }
.rollover__rows li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.95rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px dashed var(--line);
}
.rollover__rows li:last-child { border-bottom: 0; padding-bottom: 0; }
.rollover__rows b { font-variant-numeric: tabular-nums; font-weight: 600; }
.rollover__out { color: var(--accent-label); }
.rollover__out b { color: var(--accent-label); }

/* ---------------- 15 the enquiry form ---------------- */

.fld__opt {
  font-family: var(--font-mono);
  font-size: calc(var(--mono-size) - 1px);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--dim);
  margin-left: 0.35rem;
}
/* style.css styles inputs and textareas for the Sprint form but has no select
   rule, because no form in the system had one until this page. Match the
   input treatment exactly rather than letting the browser default through. */
#lockdown-form select {
  width: 100%;
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem 2.2rem 0.75rem 0.85rem;
  min-height: 48px;
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 18px) calc(50% + 2px),
    calc(100% - 13px) calc(50% + 2px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
#lockdown-form select:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* ---------------- shared odds and ends ---------------- */

/* Readability pass, 2026-09-08 - scoped to this page only (this file loads
   nowhere else), after checking actual computed sizes rather than assuming:
   mono labels already carry a site-wide +3px bump (style.css, "mono scale"),
   and .faq__a p is already the full 18px body size - neither needed
   anything more. The one genuinely small, plain-prose text this page's
   footer had was its intro paragraph (.footer__mid p, NOT .mono), at
   14.72px versus 18px everywhere else - noticeably smaller than its own
   sibling lines (the pool-price and email lines below it are already
   17.72px, since those carry .mono and already got the site-wide bump).
   Matching it to that same 17.72px brings the whole footer block to one
   consistent size rather than singling this line out further; the
   selector's specificity is lower than `.footer__mid p.mono` in style.css
   on purpose, so the two mono lines keep their own rule and are not
   double-bumped by this one. */
.footer__mid p { font-size: calc(0.92rem + 3px); }

/* Lightbox toolbar overflow on narrow phones, 2026-09-08. .sbx__bar
   (sprint.css, shared with the Sprint page - scoping the fix here rather
   than editing that shared file) lays "Next Project" out to the left and
   "Previous Project" + "Close x" out to the right via justify-content:
   space-between, with no wrap. On a phone the three text buttons together
   are wider than the panel, and because the right-hand group is pushed to
   the far edge rather than shrinking, Close silently scrolls out of the
   initial view instead of visibly cramming - a visitor has no way to
   discover it needs a horizontal scroll to find it. flex-wrap:wrap is the
   minimal fix: buttons that do not fit drop to a second row instead of
   overflowing sideways, so Close is always on screen without touching the
   shared file. Worth checking whether the Sprint page wants the same fix -
   it uses the identical component. */
@media (max-width: 560px) {
  .sbx__bar { flex-wrap: wrap; row-gap: 0.5rem; }
}

.portcard .portcard__note { margin-top: 0.9rem; color: var(--dim); }

/* The Sprint page's hero is a 16:9 photograph; this one is a board that sizes
   itself from its own contents. Release the aspect-ratio sprint.css puts on
   the hero figure so the board is not stretched to a shape it does not want. */
.hero--cal .hero__center--split { align-items: center; }

@media (max-width: 900px) {
  .hero--cal .hero__center--split { padding-bottom: 4.5rem; }
}

/* ---------------- proof band: plain horizontal strip ----------------
   Scroll-jacking removed entirely 2026-09-08, after six redesigns in one
   sitting trying to make this purely atmospheric, non-clickable photo strip
   pin and scroll-scrub itself (gaps, jumps, a strip that decoupled from the
   page's own scroll rate, a global wheel-damping zone). Trevor's own call:
   for a page whose job is closing an annual production retainer, that much
   ongoing fragility - untested trackpads, unusual displays, a future edit
   that quietly breaks a wheel listener nobody remembers is there - was
   disproportionate to what a decorative motion effect actually buys. See
   the retired module 2 in lockdown.js for the fuller history.

   What is here now is just the fallback this component always had for
   sub-900px and prefers-reduced-motion, promoted to being the ONLY version:
   a plain native horizontal scroller, unconditionally, at every width. Full
   bleed, edge to edge, breaking out of .wrap - still the one moment on the
   page where photography touches the sides of the screen, standing in for
   a hero image the page deliberately does not have. Not interactive (no
   button, no gallery click-through - "Our Work" further down is where a
   visitor who wants to click through does that), so the tile stays a plain
   <li><img><span>. */

.proofscroll { margin: 0; padding: 0; border-top: 1px solid var(--line); position: relative; }
.proofscroll__sticky { overflow: hidden; background: var(--paper); }
.proofscroll__track {
  list-style: none;
  display: flex;
  gap: 1px;
  background: var(--line);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-inline: contain;
  -webkit-overflow-scrolling: touch;
}
.proofscroll__track > li {
  flex: none;
  scroll-snap-align: start;
  width: clamp(210px, 24vw, 300px);
  position: relative;
  background: var(--paper);
  overflow: hidden;
}
.proofscroll__track img {
  display: block;
  width: 100%;
  height: clamp(170px, 19vw, 250px);
  object-fit: cover;
  /* Sources here are a mix of landscape event frames and a couple of portrait
     backdrop shots (Humanz). A dead-centre crop is right for the former and
     bites into the latter, so bias up a little across the board: it is never
     wrong for a landscape frame and it is what keeps both people's faces in a
     portrait one.
     This is the no-JS fallback value. proof-strip.js overrides it inline,
     per photo, once it runs - a stronger bias for portrait sources
     specifically (see that file's own header comment), or a manual
     data-focal override on that photo's <li> when Trevor's own eye says
     the guess is wrong. */
  object-position: center 38%;
  filter: saturate(0.92);
}
/* The caption sits ON the frame, so it needs its own ground rather than
   relying on whatever happens to be in the photograph behind it. Brand name
   only, per Trevor - no scene description, no call to action. */
.proofscroll .proofscroll__track > li .mono {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 1.4rem 0.7rem 0.55rem;
  text-align: left;
  color: #fff;
  background: linear-gradient(to top, rgba(8, 8, 7, 0.78), transparent);
  font-size: calc(var(--mono-size) - 1px);
}

@media (max-width: 899px) {
  .proofscroll__track > li { width: 62vw; }
  .proofscroll__track img { height: clamp(140px, 34vw, 200px); }
}
@media (max-width: 560px) { .proofscroll__track > li { width: 78vw; } }

/* ---- rotate mode (Trevor, 2026-09-11) ----
   Built by proof-strip.js only when .proofscroll's data-mode="rotate".
   Same full-bleed, edge-to-edge treatment as the scroll track - this
   replaces it, not sits alongside it. */
.proofscroll__rotate {
  display: grid;
  grid-template-columns: repeat(var(--ps-n, 5), 1fr);
  gap: 1px;
  background: var(--line);
}
.ps-slot {
  position: relative;
  aspect-ratio: 480 / 320;
  overflow: hidden;
  background: var(--paper);
}
.ps-slot img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: center 38%;
  filter: saturate(0.92);
  opacity: 0;
  transition: opacity 900ms ease;
}
.ps-slot img.is-active { opacity: 1; }
.ps-slot__cap {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 1;
  padding: 1.4rem 0.7rem 0.55rem;
  color: #fff;
  background: linear-gradient(to top, rgba(8, 8, 7, 0.78), transparent);
  font-size: calc(var(--mono-size) - 1px);
}

/* Trevor, 2026-09-11: "please find a nice way to include that strip in
   the mobile version of the page." Whatever count is configured for
   desktop, small screens wrap into a multi-row grid of comfortably
   sized tiles instead of squeezing that many into one cramped row. */
@media (max-width: 899px) {
  .proofscroll__rotate { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}
@media (max-width: 560px) {
  .proofscroll__rotate { grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); }
  .ps-slot__cap { font-size: calc(var(--mono-size) - 2px); padding: 1.1rem 0.55rem 0.45rem; }
}

/* Trevor, 2026-09-11: "please put a toggle for hiding the text on the
   strip images, like UNDP, Ramaphosa, or whatever." Set by proof-
   strip.js from .proofscroll's own data-show-captions attribute - one
   rule covers both modes since each caption (the client name) and its
   own dark gradient live in the same element (.mono in scroll mode,
   .ps-slot__cap in rotate mode), so hiding that element removes both
   at once, not just the text on top of a still-visible gradient. */
.proofscroll--no-captions .proofscroll__track > li .mono,
.proofscroll--no-captions .ps-slot__cap { display: none; }


/* ---------------- year examples: dates and multi-day conferences ----------------
   Added after Trevor asked for real-looking dates and for conferences to read
   as one booking rather than as separate days. A calendar with dates on it is
   also just more convincing than a bag of unlabelled chips: it invites the
   reader to hold it against their own year, which is the whole job of the
   section. The dates are illustrative and the panel lede says so. */

.evgrid .ev .ev__date {
  color: var(--accent-label);
  font-size: calc(var(--mono-size) - 1px);
  letter-spacing: 0.06em;
}
.ev { grid-template-rows: auto auto auto; }

/* A multi-day event is ONE booking, so its days are wrapped in a single
   bordered block with a shared label instead of sitting loose in the grid.
   --span is set inline per group so a three-day conference occupies three
   columns and its days stay side by side, which is what makes the run of
   consecutive dates readable at a glance. */
.evgroup {
  grid-column: span var(--span, 2);
  display: grid;
  gap: 0.45rem;
  align-content: start;
  padding: 0.5rem;
  border: 1px dashed var(--accent-label);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 5%, transparent);
  min-width: 0;
}
.evgrid .evgroup .evgroup__tag {
  color: var(--accent-label);
  font-size: calc(var(--mono-size) - 1px);
  letter-spacing: 0.05em;
}
.evgrid .evgroup .evgroup__tag b { font-weight: 700; }
.evgroup__days {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(var(--span, 2), minmax(0, 1fr));
  gap: 0.45rem;
}
/* Inside a group the day cards drop their own tint and left rule: the group's
   border is already saying "these belong together", and repeating the device
   inside it just adds noise. */
.evgroup .ev {
  background: var(--paper);
  border-left-width: 1px;
  border-left-color: var(--line);
}

@media (max-width: 620px) {
  /* The grid is a single column here, so a group cannot span anything. Its
     days stack instead, and the group border is what keeps them reading as one
     conference. */
  .evgroup { grid-column: span 1; }
  .evgroup__days { grid-template-columns: minmax(0, 1fr); }
}

/* ---------------- enquiry submit: headroom ----------------
   The submit button was sitting flush against the consent paragraph, measured
   at a 0px gap. On the primary conversion control that reads as cramped and
   makes the consent line feel like part of the button. The consent text stays
   close to the form it applies to; the button gets air of its own. */
#lockdown-form .consent { margin-bottom: 0; }
#lockdown-form #lf-submit { margin-top: clamp(1.3rem, 2.4vw, 1.9rem); }

/* ---------------- hero: the calendar slideshow ----------------
   Replaces the year board as the hero visual. Same crossfade and slow push-in
   as the Sprint page's hero so the two pages feel like one system, and the
   same reasoning behind the details:

   The rest state is the UN-zoomed frame and the transform transition is 0ms
   DELAYED past the fade, so an outgoing shot holds its zoom for the whole
   crossfade and only resets once it is invisible. Without the delay the
   outgoing frame visibly rubber-bands as it leaves. */

.calshow { margin: 0; display: grid; gap: 0.7rem; }
.calshow__frame {
  position: relative;
  /* the supplied frames are 1563x1006. Matching that exactly means object-fit
     has nothing to crop, so no part of a calendar is ever cut off. */
  aspect-ratio: 1563 / 1006;
  width: 100%;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
.calshow__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 0;
  transform: scale(1);
  transition: opacity 1400ms ease, transform 0ms linear 1400ms;
}
.calshow__shot.is-on {
  opacity: 1;
  transform: scale(1.05);
  transition: opacity 1400ms ease, transform 7600ms linear;
}
.calshow figcaption { color: var(--dim); }

@media (prefers-reduced-motion: reduce) {
  /* no drift, no crossfade: the first frame simply stands still */
  .calshow__shot, .calshow__shot.is-on { transition: none; transform: none; }
}

/* ---------------- hero: bigger visual, bigger copy ----------------
   Trevor asked for a larger hero image and larger copy beside it, 2026-09-07.

   The Sprint page's hero splits almost evenly (1fr / 1.02fr) because its
   visual is a 16:9 film still that reads fine small. These calendar frames are
   detailed: they contain a readable twelve-month plan, and the whole point is
   that a visitor can see the year laid out. At half width the month labels
   turn to mush, so the visual takes the larger share here and the container
   widens to give both columns room to grow rather than trading one against
   the other. */
.hero--cal .hero__center--split {
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.3fr);
  width: min(100%, 1440px);
}
.hero--cal .hero__center--split .hero__copy { max-width: 38rem; }

/* Two classes beats the (0,1,1) base rule in style.css. */
.hero--cal .hero__copy h1 {
  font-size: clamp(1.55rem, 2.6vw, 2.35rem);
  line-height: 1.18;
  letter-spacing: -0.018em;
}
.hero--cal .hero__copy .sub {
  font-size: clamp(1.08rem, 1.6vw, 1.35rem);
  line-height: 1.5;
  max-width: 36rem;
}

@media (max-width: 900px) {
  /* Stacked, so the split ratio is meaningless and the copy should use the
     full column again. */
  .hero--cal .hero__center--split { grid-template-columns: minmax(0, 1fr); }
  .hero--cal .hero__center--split .hero__copy { max-width: none; }
}

/* ---------------- "how it works" arrives early ----------------
   Its own reveal, separate from the shared .rv (style.css).

   Simplified back down 2026-09-08: the previous few passes synced this to
   the proof band's own scroll-scrubbed progress (a negative top margin
   pulling it up to overlap the still-pinned strip, --how-o/--how-y written
   per frame). All of that went away with the scroll-jacking itself - see
   the retired module 2 in lockdown.js. This is back to a plain
   IntersectionObserver-triggered fade-and-rise (module 3), the same shape
   it had before any of that experimentation started. --how-o/--how-y stay
   as the mechanism (rather than plain opacity/transform in the .in rule)
   only because nothing forces them out - either shape works fine here now. */
.howreveal { opacity: var(--how-o, 0); transform: translateY(var(--how-y, 40px)); }
.howreveal.in {
  --how-o: 1;
  --how-y: 0px;
  transition: opacity 0.9s ease, transform 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}
.howreveal[data-d="1"].in { transition-delay: 0.08s; }
.howreveal[data-d="2"].in { transition-delay: 0.16s; }
@media (prefers-reduced-motion: reduce) {
  .howreveal { opacity: 1; transform: none; }
  .howreveal.in { transition: none; }
}
