/* ==========================================================================
   BEACON 2026 — 2026 edition theme
   Loaded AFTER beacon-2023-*.webflow.shared.css, and ONLY by /index.html.
   The Webflow sheet stays untouched: its .w-* rules are the visual half of
   the Webflow JS runtime (nav overlay, slider), and it is shared with the
   2023/2024/2025 archive pages, which deliberately keep the dark theme.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   Names follow the existing --group--name-weight convention from the shared
   sheet's :root block. Only --background--red-300 is redeclared: the
   inherited value is the legacy 2024 red (#f63440). The current brand red is
   #f5333f, taken from the BEACON 2025 logo (logo_2025v2.svg, .cls-2).
   -------------------------------------------------------------------------- */
:root {
  --background--red-300: #f5333f;        /* was #f63440 (2024) — corrected */

  --surface--page:       #fff;
  --surface--cta:        #fcfcfc;
  --ink:                 #000;
  --ink--invert:         #fff;
  --rule--faint:         rgba(245, 51, 63, .32);   /* red hairline, FAQ + agenda */
  --radius--pill:        99px;
  --border--strong:      2px;

  /* Type scale — archive-anchored.
     Every value below is the size the equivalent element uses on the 2023/24/25
     pages (see beacon-2023-*.webflow.shared.css), stepped at the same
     breakpoints. The previous scale was derived from Figma's 1728-wide
     artboard, which is a presentation frame rather than a viewport, and came
     out roughly 2x on every display size. */
  --type--display:   40px;   /* .section-heading-copy / -agenda / -faq-copy / -sponsors */
  /* The event date. This is the one step of the scale NOT anchored to the
     archive (which sets 40/32/28/24): the line carries the weekday now, and
     "SATURDAY, 14 NOVEMBER 2026" at the archive's size dominated the hero.
     One step down at every stop, still 1.4-1.6x the venue line under it.
     Nothing else uses --type--hero. */
  --type--hero:      32px;   /* .countdown-text-heading-2026  (event date) */
  --type--hero-sub:  20px;   /* .countdown-subtext-2026       (venue) */
  --type--cta:       16px;   /* .button-register-2025 */
  --type--lead:      20px;   /* .section-body / .section-body-copy */
  --type--body:      20px;   /* .agenda-table-text */
  --type--nav:       18px;   /* .nav-link-4 (20px at >=1440) */
  --type--detail:    18px;   /* .paragraph, .speaker-name, .speaker-role */
  --type--small:     16px;   /* .text-block---italic */
  /* 14px is the floor everywhere: it is the smallest size APCA defines a
     body-text anchor for (14px/700 at Lc 75). Below it there is no
     published size/contrast pairing at any contrast, so nothing on the
     page goes under it — this token used to step 13/12/11. */
  --type--micro:     14px;   /* tags, tier labels — no archive equivalent */
  --type--tab:       20px;   /* agenda tabs — no archive equivalent. One step above
                                the CTA it used to track: the tabs now carry
                                track NAMES and an icon, not "Track 1". */

  /* Display sizes with no archive equivalent. */
  --type--carousel:    28px; /* .countdown-text-heading-copy-copy */

  /* Layout. The gutter IS the dot band: dotfield.js reads these two values to
     work out where it may draw, so the reserved space and the animation can
     never drift apart. */
  --content--max: 1120px;
  --page--gutter: 140px;
  /* Sub-header -> the section's first content block. One value for all six
     sections; they had five different clamps giving 35-58px. */
  --space--lead: clamp(28px, 3.6vw, 62px);
  /* How wide the hairline rules run, as a share of the section's content box.
     They used to fill it, which put them the width of a gutter from the edge —
     42px at 390. The comp insets its FAQ rules 14.8% of the frame each side;
     88% of the box reproduces that at every width. */
  --rule--span: 88%;
  /* The DOT group is capped tighter than the text column. At --content--max the
     cards came out 394px on a desktop, which read as oversized; every part of
     the arrangement is a percentage of this box, so shrinking it scales the
     cards, the gaps and the connectors together and keeps the comp's geometry
     intact. */
  --dot-cards--max: 900px;
  /* Vertical padding on every section. Sections butt straight up against each
     other, so this doubles into the gap between them: at 7vw it was 202px
     between sections at 1440. One token so the page keeps a single rhythm. */
  --space--section: clamp(40px, 5vw, 88px);
  /* The nav is pinned to this height and the hero subtracts it to fill the
     first screen, so the two cannot drift apart. */
  --nav--h: 64px;
}

@media screen and (min-width: 1440px) {
  :root { --type--nav: 20px; }          /* .nav-link-4 at >=1440 */
}

@media screen and (max-width: 1439px) {
  :root { --page--gutter: 96px; }
}

@media screen and (max-width: 991px) {
  :root {
    --type--display: 28px;
    --type--hero: 28px;
    --type--hero-sub: 16px;
    --type--cta: 18px;                  /* archive is larger here than on desktop */
    --type--lead: 18px;
    --type--body: 18px;
    --type--nav: 16px;
    --type--detail: 16px;
    --type--small: 15px;
    --type--tab: 18px;
    --page--gutter: 72px;
    --nav--h: 84px;                     /* hamburger row is taller here */
  }
}

@media screen and (max-width: 767px) {
  :root {
    --type--display: 24px;
    --type--hero: 24px;
    --type--small: 14px;
    --type--tab: 17px;
    --page--gutter: 56px;
  }
}

@media screen and (max-width: 479px) {
  :root {
    --type--hero: 20px;
    --type--hero-sub: 14px;
    --type--cta: 16px;
    --type--lead: 16px;
    --type--body: 16px;
    --type--tab: 15px;
    --type--carousel: 16px;
    --page--gutter: 42px;
  }
}


/* --------------------------------------------------------------------------
   2. Page ground
   -------------------------------------------------------------------------- */
.body {
  background-color: var(--surface--page);
  color: var(--ink);
}

/* The shared sheet sets `cursor: none` globally for the 2023 custom cursor,
   which this page does not ship. Restore a real pointer. */
body,
body a,
.body a { cursor: auto; }
.body a,
.body button,
.body [role="button"],
.body .w-button { cursor: pointer; }

/* Content sits above the dot field (added in a later step). */
/* Every section needs a stacking context above the fixed dot-field canvas —
   except a <dialog>, which lives in the top layer and is above everything
   anyway. Including it here beat the UA's `dialog:modal { position: fixed }` on
   specificity, so the modal was laid out in flow at the top of the document and
   showModal() scrolled the whole page up to reach the focused close button. */
/* `> main >` is not decoration: the sections now live inside a <main>
   landmark, and `.cover-2025` gets its `position: relative` from nowhere
   else — `.hero-decoration` is `position:absolute; inset:0` and would
   otherwise resolve against <main> and cover the whole page. Any new
   wrapper around the sections has to be added here too. */
.body > *:not(dialog),
.body > main > * { position: relative; z-index: 1; }

/* --------------------------------------------------------------------------
   3. Shared primitives
   -------------------------------------------------------------------------- */

/* Section header — Rubik Black, centred, black. */
.section-heading-copy,
.section-heading-agenda,
.section-heading-sponsors,
.section-heading-faq-copy,
.section-heading-ticket-copy {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 900;
  font-size: var(--type--display);
  line-height: 1.1;
  letter-spacing: -.01em;
  text-align: center;
  text-transform: uppercase;
  /* The ONLY thing between a section heading and its sub-header — the wrappers'
     legacy row-gap is zeroed below so this is the single source of that spacing
     in every section. In em, so it holds when --type--display drops at the
     narrow breakpoints. */
  margin: 0 0 .4em;
}

/* Legacy puts a flex row-gap on two of the six section wrappers (16px on the
   speakers/committee copy block, 64px on the ticket section), which is why the
   heading-to-sub-header gap measured 7 / 23 / 71px across the page and the
   ticket section looked so loose. Spacing here comes from margins only. */
.div-block-speakers-copy,
.grab-your-ticket-copy { row-gap: 0; }

/* Section description / lead paragraph. */
.section-body,
.section-body-copy,
.text-block---italic,
.contact-copy {
  /* Legacy pulls .text-block---italic up by 12-20px, which overlaps the
     section heading now that headings are archive-sized, and adds a 20-48px
     bottom margin below 992px that stacked on top of --space--lead. Both
     zeroed: the heading's margin-bottom and --space--lead are then the only
     two things setting vertical rhythm around a sub-header. */
  margin-top: 0;
  margin-bottom: 0;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-style: normal;
  font-size: var(--type--lead);
  line-height: 1.35;
  text-align: center;
  max-width: 24em;
  margin-inline: auto;
}

.text-span-4,
.text-span-5 { color: var(--background--red-300); }

/* CTA pill — white surface, 2px black border, black label.
   Hover fills red with a red border and white label (confirmed). */
.beacon-cta,
.button-register-2025,
.button-reel-2025 {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-height: var(--cta--height, 69px);
  padding: 0 var(--cta--padx, 40px);
  background-color: var(--surface--cta);
  border: var(--border--strong) solid var(--ink);
  border-radius: var(--radius--pill);
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--cta);
  line-height: 1.1;
  letter-spacing: .01em;
  text-align: center;
  text-decoration: none;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease;
}

.beacon-cta:hover,
.beacon-cta:focus-visible,
.button-register-2025:hover,
.button-register-2025:focus-visible,
.button-reel-2025:hover,
.button-reel-2025:focus-visible {
  background-color: var(--background--red-300);
  border-color: var(--background--red-300);
  color: var(--ink--invert);
}

/* Red-filled variant (BUY NOW). */
.beacon-cta--solid {
  background-color: var(--background--red-300);
  border-color: var(--background--red-300);
  color: var(--ink--invert);
}
.beacon-cta--solid:hover,
.beacon-cta--solid:focus-visible {
  background-color: var(--background--red-400);
  border-color: var(--background--red-400);
  color: var(--ink--invert);
}

/* Visible focus ring — the Webflow sheet suppresses outlines. */
.body a:focus-visible,
.body button:focus-visible,
.body [tabindex]:focus-visible {
  outline: 2px solid var(--background--red-300);
  outline-offset: 3px;
}

/* --------------------------------------------------------------------------
   4. Nav
   Figma: better>sg mark left; Agenda / Speakers / Committee / Past Editions.
   data-collapse is "medium", so Webflow shows inline links >= 992px and the
   hamburger below. Both paths are styled here.
   -------------------------------------------------------------------------- */
.navbar-copy {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  background-color: var(--surface--page);
  justify-content: space-between;
  padding: 0;
}

/* Above the sections, which `.body > *` all sets to z-index 1. Equal z-index
   means DOM order decides, so the hero — the next sibling — was painting and
   hit-testing over the Past Editions panel. The panel stayed visible because the
   hero is transparent, but every pointer event below the nav's bottom edge went
   to the hero, which dropped :hover and closed the menu.
   Written as `.body > .navbar-copy` deliberately: `.navbar-copy` alone (0,1,0)
   loses to `.body > *:not(dialog)` (0,1,1) and does nothing. */
.body > .navbar-copy { z-index: 40; }

.div-block-10-copy {
  margin-left: 0;                     /* legacy offsets the nav bar by -20px */
  background-color: transparent;
  width: 100%;
  max-width: none;
}

.div-block-11 {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 14px var(--page--gutter);
  min-height: var(--nav--h);
}

/* Both overrides are load-bearing since the ticket pill came back. Legacy pins
   this block to 580px around a 144px logo, and with the pill in the row that
   slack pushed the nav links 149px past the right gutter and off the screen;
   legacy also hangs a 24px left margin on it below 1440, which is the last 24px
   the row needs at 992 (and which put the logo off the gutter that one tier). */
.div-block-12 { display: flex; flex: 0 0 auto; align-items: center; width: auto; margin-left: 0; }
.div-block-13 { display: flex; flex: 0 0 auto; align-items: center; gap: clamp(14px, 1.6vw, 28px); padding-right: 0; margin-right: 0; width: auto; }


/* Top-bar ticket pill, shown from 992 up — the same width at which Webflow
   swaps the hamburger for inline links. The shared sheet hides it at EVERY
   width (the original only revealed it on scroll, through an interaction this
   page does not run) and pins it to a fixed 180/200/220/60px box, so both have
   to be beaten with `!important`. BELOW 992 it stays hidden, and that is a fit
   result, not a preference: at 390 the row is logo 154 + hamburger 56 inside a
   306px content box, which leaves 96px for a pill whose label needs 154 at the
   14px type floor — it pushed the hamburger 66px off-screen. The archive pages
   get away with it on a phone by dropping to 12px type and a zero gutter, which
   this page does not do. The nav is not sticky, so on a phone the pill would
   only ever be visible on the first screen, where the hero CTA already is. */
@media screen and (min-width: 992px) {
  .nav-top-bar-button {
    display: inline-flex !important;
    align-items: center;
    /* Source order is [pill][hamburger][links]; the CTA ends the row. */
    order: 1;
    width: auto !important;
    height: auto !important;
    /* Legacy hangs margin-left:20px / margin-right:8px on it, which double-spaced
       it from the links and held it 8px off the right gutter. */
    margin: 0;
    padding: 10px 20px !important;
    border-radius: var(--radius--pill);
    white-space: nowrap;
    font-size: var(--type--micro);
  }
}

/* Legacy sets a global `a { padding-left: 20px }`, which shifts any image
   inside a block link off-centre by half that. .w-inline-block is Webflow's
   class for exactly these block-level links (nav logo, footer logo, socials). */
.body .w-inline-block { padding-left: 0; }
.link-block-6 { padding: 0; }

.nav-logo,
.image-9-copy {
  display: block;
  width: auto;
  height: 34px;
  max-width: none;
}

/* Legacy sets flex-flow:column and a #030101 ground — both must be undone. */
.nav-menu-4 {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  gap: clamp(14px, 2.3vw, 40px);
  padding: 0;
  background-color: transparent;
}

/* Legacy spaces these with margin-left: 48px / 98px / 24px per breakpoint. */
.nav-link-4 {
  margin: 0;
  padding: 6px 0;
  overflow: visible;
  background-color: transparent;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--nav);
  line-height: 1.2;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
  white-space: nowrap;
}

.nav-link-4:hover,
.nav-link-4.w--current { color: var(--background--red-300); }

/* Hamburger (below 992px). */
.menu-button-3 { background-color: transparent; padding: 8px; }
.menu-button-3.w--open { background-color: transparent; }
.icon-4 { color: var(--ink); }

/* Past Editions dropdown — light-theme repaint of the injected block.
   Structure and toggle JS are unchanged. */
.editions-toggle { color: var(--ink); padding: 6px 0; }
.editions-toggle:hover { color: var(--background--red-300); }

.editions-panel {
  background: var(--surface--page);
  border: 1px solid var(--rule--faint);
  border-radius: 10px;
  padding: 6px 0;
}

.editions-panel-link {
  color: var(--ink) !important;
  font-family: Rubik, sans-serif !important;
  font-size: var(--type--nav) !important;
  font-weight: 500 !important;
  letter-spacing: 0;
  padding: 8px 20px !important;
  /* Legacy centres every .nav-link-4; the panel is a sub-list of a left-aligned
     menu, so it reads as an indent, not a column of its own. This lived in the
     >=992 block only, which left the mobile overlay centred. */
  text-align: left !important;
}
.editions-panel-link:hover { color: var(--background--red-300) !important; }
.editions-panel-link.current { opacity: .4; }

/* Desktop: float the panel instead of pushing the bar open. */
@media screen and (min-width: 992px) {
  .editions-hover { width: auto; }
  .editions-panel {
    position: absolute;
    top: 100%;
    right: 0;
    min-width: 190px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, .10);
    z-index: 60;
  }
}

/* Mobile: Webflow drops the menu into an overlay. Repaint it light. */
@media screen and (max-width: 991px) {
  .w-nav-overlay .nav-menu-4,
  .nav-menu-4.w--open {
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 8px var(--page--gutter) 24px;
    background-color: var(--surface--page);
    border-bottom: 1px solid var(--rule--faint);
  }
  /* "Past Editions" used to be the only 18px/50px-tall row in a menu of 16px/
     31px ones. It was not meant to be: the rule that enlarged the real links,
     `.nav-menu-4.w--open .nav-link-4`, never matched anything. Webflow puts
     `w--open` on the hamburger BUTTON and moves the menu into `.w-nav-overlay`
     — which is why the two-selector rule above needs both forms. The toggle's
     own copy of that treatment carried no such guard, so it alone applied.
     Both are gone now: the toggle is a `.nav-link-4`, so it takes --type--nav
     and the base 6px padding exactly like its siblings. */
  .editions-hover { width: 100%; }
  .editions-panel { border: 0; border-radius: 0; padding: 0 0 4px; }
  /* Size comes from --type--nav via the base rule; only the indent differs. */
  .editions-panel-link { padding: 10px 0 10px 18px !important; }
}

/* --------------------------------------------------------------------------
   5. FAQ
   Figma: header, contact line, then four rows — question left, chevron right,
   red hairline under each.
   -------------------------------------------------------------------------- */
.faq-copy {
  background-color: transparent;
  padding: var(--space--section) var(--page--gutter);
}

.contact-copy { margin-bottom: var(--space--lead); }

.faq_accordion-copy {
  width: var(--rule--span);       /* legacy pins this to 900px */
  max-width: var(--content--max);
  margin: 0 auto;
  border-bottom: 1px solid var(--rule--faint);
}

.faq_question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: clamp(18px, 2.2vw, 34px) 0;
  background-color: transparent;
  cursor: pointer;
}

.faq-question-text {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 600;
  font-size: var(--type--body);
  line-height: 1.3;
  text-align: left;
}

.faq-icon-wrapper {
  flex: 0 0 auto;
  width: clamp(26px, 2.6vw, 45px);
  height: clamp(26px, 2.6vw, 45px);
  color: var(--ink);
  transition: transform .25s ease, color .25s ease;
}

/* Legacy .code-embed is `color:#fff; width:3rem; height:3rem` — the dark-theme
   chevron. It sits between the wrapper and the path, so `fill="currentColor"`
   resolved to white on white, and the 3rem box overflowed the question row. */
.faq-icon-wrapper .code-embed {
  color: inherit;
  width: 100%;
  height: 100%;
}
.faq-icon-wrapper svg { display: block; width: 100%; height: 100%; }

.faq_question.open .faq-icon-wrapper {
  transform: rotate(180deg);
  color: var(--background--red-300);
}

/* .faq_answer height is animated inline by the Webflow interaction —
   never set height here. */
.faq_answer { overflow: hidden; }

.faq_answer .paragraph {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: var(--type--detail);
  line-height: 1.6;
  text-align: left;
  padding-bottom: clamp(16px, 1.8vw, 30px);
  max-width: 62em;
}

.faq_answer .link {
  color: var(--background--red-300);
  text-decoration: underline;
  padding-left: 0;
}
.faq_answer .hashtag {
  color: var(--background--red-300);
  text-decoration: none;
  padding-left: 0;
}

/* --------------------------------------------------------------------------
   6. Footer
   Figma: social icons centred above a large better>sg wordmark.
   -------------------------------------------------------------------------- */
.footer {
  background-color: transparent;
  /* Legacy gives the footer 1px #242424 top AND bottom borders, which were
     nearly invisible against its old #111 ground but read as full-bleed black
     rules on white. */
  border: 0;
  padding: clamp(40px, 5vw, 90px) var(--page--gutter) clamp(48px, 6vw, 100px);
}

.footer .container {
  display: flex;
  flex-direction: column-reverse;   /* icons above logo */
  align-items: center;
  gap: clamp(20px, 2.4vw, 40px);
  max-width: none;
}

.footer-detail-left,
.footer-detail-right {
  display: flex;
  justify-content: center;
  align-self: center;                 /* legacy sets flex-start on -left */
  width: 100%;
  min-height: 0;
}

.footer-logo {
  width: auto;
  height: 48px;              /* ~200px wide — archive .footer-logo width */
  max-width: none;
}

.social-icon-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 1.1vw, 19px);
}

.social-link {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;                          /* legacy adds margin-left:16px */
  padding: 0;                         /* legacy adds padding:10px */
  width: clamp(34px, 3.4vw, 59px);
  height: clamp(34px, 3.4vw, 59px);
  background-color: #262626;
  border-radius: 10px;
  transition: background-color .18s ease;
}
.social-link:hover { background-color: var(--background--red-300); }

.social-icon-linkedin,
.social-icon-facebook,
.social-icon-instagram { width: 58%; height: auto; }

/* --------------------------------------------------------------------------
   7. Hero
   Figma: white ground, dot field, three-colour logo, date, venue, one CTA.
   Legacy .cover-2025 paints a background image at five separate breakpoints;
   all of them have to be cleared.
   -------------------------------------------------------------------------- */
.cover-2025,
.cover-2025:hover {
  background-color: transparent;      /* let the dot field show through */
  margin-bottom: 0;                   /* legacy pulls the next section up 20-59px */
  background-image: none;
  cursor: auto;
  margin-top: 0;
  /* Fill the first screen and centre in it (legacy already sets
     justify-content: center). Sized to the space the nav leaves rather than a
     fixed top padding, so the block stays centred at any viewport height
     instead of only at the one it was tuned on. The padding below is the floor
     for viewports too short to centre in. */
  min-height: calc(100vh - var(--nav--h));
  min-height: calc(100svh - var(--nav--h));
  padding: clamp(48px, 7vw, 120px) var(--page--gutter) clamp(40px, 6vw, 100px);
  row-gap: clamp(20px, 2.4vw, 40px);
  column-gap: 0;
}

@media screen and (max-width: 991px) {
  .cover-2025 { background-image: none; }
}

/* CTA pill box follows the archive button: 69px tall, 40px side padding,
   tightening to 53px / 20px on small screens. */
@media screen and (max-width: 991px) {
  .beacon-cta, .button-register-2025, .button-reel-2025 { --cta--padx: 20px; }
}
@media screen and (max-width: 479px) {
  .beacon-cta, .button-register-2025, .button-reel-2025 { --cta--height: 53px; }
}
@media screen and (max-width: 767px) {
  .cover-2025 { background-image: none; }
}
@media screen and (max-width: 479px) {
  .cover-2025 { background-image: none; }
}

/* Hero logo stage — canvas draws the trace, the SVG fades in on top.
   Ratio is 141/937 from the logo viewBox. */
/* 2004:39 is 1080 wide in a 1728 frame = 62.5vw. Hold that proportion up to
   the comp's own size, then let it go full-bleed on narrow screens. */
.hero-logo-stage {
  position: relative;
  width: min(100%, clamp(300px, 62.5vw, 1080px));
  margin: 0 auto;
}
@media screen and (max-width: 767px) {
  .hero-logo-stage { width: 100%; }
}
/* ONE artwork at every width. beacon-2026-lockup.svg is a two-line lockup that
   carries CONNECTING THE DOTs inside it, so the phone-only stacked variant, its
   --hero-stacked breakpoint flag, the second ratio and the separate tagline
   element are all gone with it — four things that had to agree, replaced by
   one file that cannot disagree with itself.

   35.625% is 285 / 800, the artwork's own viewBox, printed by
   scripts/prepare-lockup.py so it cannot be mistyped here. */
.hero-logo-mark { position: relative; width: 100%; }
.hero-logo-ratio { width: 100%; padding-top: 35.625%; }  /* 285 / 800 */

/* The live SVG the reveal animates, injected by index.html. It sits over the
   <img>, which stays at opacity 0 for the whole animated path — the end of the
   reveal IS the finished artwork now, so there is nothing to cross-fade TO and
   the handover the old canvas needed has no equivalent here. The <img> is only
   ever shown when the animation does not run at all. */
.hero-logo-live {
  position: absolute;
  inset: 0;
  display: block;
}
.hero-logo-live > svg { display: block; width: 100%; height: 100%; }
.hero-logo-final {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 1.1s ease;
  pointer-events: none;
}

/* Date + venue. */
.text-and-buttons-wrapper-2025 {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0;
}

.countdown-text-2025 {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: clamp(16px, 2.2vw, 38px);
}

.countdown-text-heading-2026,
.countdown-subtext-2026 {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  line-height: 1.24;
  text-align: center;
  padding: 0;
  margin: 0;
}

/* Archive gives the date and the venue different weight (40px vs 20px). */
.countdown-text-heading-2026 { font-size: var(--type--hero); }
/* The line is one line from 767 up and two below it. Left to itself the break
   fell inside the date and stranded "2026" on a line of its own; holding the
   date together makes the comma the only place it can break, which is where a
   reader would put it anyway. */
.countdown-date { white-space: nowrap; }
.countdown-subtext-2026 { font-size: var(--type--hero-sub); margin-top: .35em; }

/* Hero CTA row — single pill in the comp. */
.hero-cta-row {
  display: flex;
  justify-content: center;
  margin-top: clamp(22px, 3.2vw, 55px);
  margin-bottom: 0;
}
/* `min-width` alone let the pill grow to its label's max-content and spill past
   the gutters — at 320 "GRAB YOUR TICKET WHILE YOU CAN" reached both viewport
   edges. `max-width` caps it at the row, so a long label wraps inside the pill
   instead of widening it. */
.hero-cta-row .button-register-2025 { margin: 0; min-width: min(100%, 638px); max-width: 100%; }

/* Countdown stays in the DOM (fb-countdown-target is the documented hook)
   but is hidden: the 2026 comp has no countdown. Delete this rule to restore. */
.clock-countdown { display: none; }

/* Legacy hero-adjacent backgrounds that must not reappear on white. */
.tickets-and-speakers { background-image: none; background-color: transparent; }
.section { background-color: transparent; }

/* --------------------------------------------------------------------------
   8. Section grounds
   Every legacy section paints a dark background or image. Clear them all so
   the white page (and the dot field behind it) shows through.
   -------------------------------------------------------------------------- */
.highlights-2025,
.grab-your-ticket-copy,
.speaker-company-copy,
.agenda-copy,
.connect-with-our-sponsors-2025,
.comittee,
.faq-copy,
.div-block-24,
.div-block-24-copy,
.div-block-agenda-copy,
.div-block-speakers-copy,
.div-block-grab-your-ticket-copy,
.div-block-connect-with-our-sponsors-copy {
  background-color: transparent;
  background-image: none;
}

.highlights-2025,
.grab-your-ticket-copy,
.speaker-company-copy,
.agenda-copy,
.connect-with-our-sponsors-2025,
.comittee {
  padding: var(--space--section) var(--page--gutter);
}

/* --------------------------------------------------------------------------
   9. Agenda — tabbed tracks
   Figma: pill tabs (2004:329/331/333/335, 62px apart), then dense rows of
   red time / black title / black tag pills, split by hairline rules
   (2004:338, 2004:344) running 192 -> 1536 in the 1728 frame.
   -------------------------------------------------------------------------- */
.agenda-tabs { margin-top: var(--space--lead); }

/* The tabs used to be four short labels ("Overview", "Track 1"...) on a row
   that scrolled horizontally and never wrapped. They now carry track NAMES and
   an icon, and three of those do not fit a phone — nor a 768 tablet, where the
   row overran its box by 64px and hid the third track behind a gesture. So the
   row WRAPS and centres instead: above ~900 it is still one line, and narrower
   it stacks. Wrapping also means a fourth track is a markup-only change, which
   the scrolling row could not claim. */
.agenda-tablist {
  display: flex;
  flex-flow: row wrap;
  justify-content: center;
  gap: 10px clamp(10px, 3.59vw, 62px);   /* 62px column gap @1728 */
  width: fit-content;                 /* shrink to the tabs, then centre */
  max-width: min(var(--content--max), 100%);
  margin: 0 auto;
  padding: 4px 2px 10px;
}

.agenda-tab {
  flex: 0 0 auto;
  appearance: none;
  /* Icon and label on one line. `inline-flex`, not `flex`, so the pill still
     shrink-wraps its content inside the wrapping row. */
  display: inline-flex;
  align-items: center;
  gap: clamp(8px, .85vw, 14px);
  padding: clamp(12px, 1.05vw, 18px) clamp(18px, 1.85vw, 32px);
  background-color: var(--surface--page);
  border: var(--border--strong) solid var(--background--red-300);
  border-radius: var(--radius--pill);
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--tab);
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color .16s ease, color .16s ease;
}

/* The three track icons are stroked in `currentColor`, so they invert with the
   pill on selection and need no second colour declared anywhere. Sized off the
   label rather than in px, so they step with --type--tab at every breakpoint. */
.agenda-tab-icon {
  flex: 0 0 auto;
  width: 1.5em;
  height: 1.5em;
  overflow: visible;                  /* a 1.7-wide stroke on the 24 box edge */
}

.agenda-tab[aria-selected="true"] {
  background-color: var(--background--red-300);
  color: var(--ink--invert);
}

.agenda-tab:hover:not([aria-selected="true"]) { background-color: rgba(245, 51, 63, .08); }

/* Fluid height: panels are siblings and only the active one is rendered.
   No fixed height, no clipping, no leftover space. */
.agenda-panels { max-width: var(--content--max); margin: 22px auto 0; }
.agenda-panel:focus { outline: none; }

.agenda-row {
  display: grid;
  grid-template-columns: minmax(78px, 34%) 1fr;
  gap: 0 clamp(12px, 2vw, 36px);
  align-items: start;
  padding: clamp(14px, 1.85vw, 32px) 0 clamp(14px, 1.85vw, 32px) clamp(8px, 5.65%, 76px);
  border-bottom: 1px solid var(--rule--faint);
  width: var(--rule--span);
  margin-inline: auto;
}
.agenda-row:last-child { border-bottom: 0; }

.agenda-time {
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--body);
  line-height: 1.35;
}

.agenda-title {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--body);
  line-height: 1.33;
}

.agenda-note {
  margin-top: .45em;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  /* Prose, not a label — --small bottoms out at 14px, --detail at 16px. */
  font-size: var(--type--detail);
  line-height: 1.4;
  opacity: .78;
}

.agenda-tag {
  display: inline-block;
  vertical-align: middle;
  margin: 0 0 .18em .5em;
  padding: .28em 1em;
  background-color: var(--ink);
  border-radius: var(--radius--pill);
  color: var(--ink--invert);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: var(--type--micro);
  line-height: 1.3;
  white-space: nowrap;
}

.agenda-empty {
  padding: clamp(26px, 3.5vw, 60px) 0;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: var(--type--detail);
  text-align: center;
  opacity: .7;
}

/* Partner logos under the Main Stage rows.
   The programme past the opening keynote is not fixed yet, so the panel names
   the organisations sharing rather than inventing session titles.

   THE ROW IS OPTICALLY SIZED, NOT BOX-SIZED, and one common max-height is the
   thing that does not work. These seven aspect ratios run 0.92 (UNFOLD Asia's
   near-square stacked lockup) to 6.11 (the DCC horizontal lockup), so at a
   shared 52px the DCC mark runs 318px wide while UNFOLD is a 48px stamp beside
   it. `--k` below is each logo's height as a multiple of `--partner-band`,
   built from two measurements of the TRIMMED artwork (both printed by
   scripts/make-partner-logos.py, which trims each source to its own ink first
   -- the numbers are meaningless while one logo carries 12% of baked-in margin
   and another a third):

     1. EQUAL AREA, damped. Raw equal-area over-rewards a square mark: it wants
        UNFOLD 94px tall against DCC's 36. Damped to the 0.65 power.
     2. EQUAL INK, damped. Box area is not weight -- alpha coverage inside the
        trimmed box runs 16.9% (Schemes.sg, a hairline serif) to 63.5% (OGS,
        three filled discs), so two marks of equal box area can differ nearly
        fourfold in how much black they put on the page. Corrected by
        (0.32 / coverage) ^ 0.25, 0.32 being the middle of that range.

   Both exponents are deliberately well under 1: each rule is a nudge toward
   its target, not an attempt to hit it. Nothing here is tuned by eye.

   `--partner-band` is the height a 3:1 logo gets; every other mark is that
   times its own `--k`, capped in width so the widest cannot run away. The row
   wraps rather than scrolls, for the same reason the tablist above it does. */
.agenda-partners {
  margin-top: clamp(26px, 3.2vw, 54px);
  width: var(--rule--span);
  margin-inline: auto;
}

.agenda-partners-lead {
  margin: 0 auto clamp(20px, 2.4vw, 40px);
  max-width: 46em;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: var(--type--detail);
  line-height: 1.45;
  text-align: center;
  opacity: .78;
}

.agenda-partners-list {
  --partner-band: clamp(32px, 3.6vw, 52px);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: clamp(18px, 2.4vw, 38px) clamp(14px, 3.2vw, 52px);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The slot reserves the TALLEST mark's band at every width, so a wrapped row
   does not jump in height when the square lockup lands on it. */
.agenda-partner {
  display: flex;
  align-items: center;
  justify-content: center;
  height: calc(var(--partner-band) * 1.4);
}

/* `padding: 0` is load-bearing. The shared Webflow sheet gives EVERY <a> a bare
   `padding-left: 20px` (for the 2023 nav), which put 20px of dead space on the
   left of each logo -- 140px across the row, enough to cost a whole column: at
   320 the row broke to six lines instead of four, and every mark sat visibly
   off-centre in its slot. .sponsor-slot--logo dodges this by setting the
   padding shorthand; this does it explicitly. */
.agenda-partner-link {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  padding: 0;
  transition: opacity .18s ease;
}
.agenda-partner-link:hover { opacity: .62; }

/* HEIGHT IS THE DEFINITE DIMENSION AND WIDTH FOLLOWS FROM IT. Releasing both
   to auto and capping with max-height/max-width looks equivalent and is not:
   these load lazily, and an image that has not loaded yet has no intrinsic
   size, so `auto` resolved to 0x0 and every logo reserved NO box -- the row
   collapsed to 20px slots until the pictures arrived. With `height` definite
   the browser derives the width from the ratio the width/height ATTRIBUTES
   supply, before a byte of the image is fetched. The max-width is a guard for
   an unusually wide replacement, not the normal path; nothing hits it today. */
.agenda-partner-mark {
  display: block;
  height: calc(var(--partner-band) * var(--k, 1));
  width: auto;
  max-width: calc(var(--partner-band) * 5.6);
  object-fit: contain;
}

/* aspect ratio, ink coverage -> k. Re-run scripts/make-partner-logos.py after
   replacing a logo; it prints this table. */
.agenda-partner--dcc         { --k: .80; }   /* 6.11 : 1   31.0% ink */
.agenda-partner--ogs         { --k: .83; }   /* 3.18 : 1   63.5% ink */
.agenda-partner--minds       { --k: .83; }   /* 4.05 : 1   46.0% ink */
.agenda-partner--schemes     { --k: 1.06; }  /* 4.07 : 1   16.9% ink */
.agenda-partner--labs65      { --k: 1.08; }  /* 2.49 : 1   29.7% ink */
.agenda-partner--unfold      { --k: 1.30; }  /* 0.92 : 1   51.6% ink */
.agenda-partner--care-corner { --k: 1.35; }  /* 1.79 : 1   18.7% ink */

/* --------------------------------------------------------------------------
   10. "DOT" value cards — Discover / Orchestrate / Transform
   Figma 2004:247/:248/:249 with letters 2004:257/:264/:265. Rotated rounded
   squares, thin black border, huge black letter, red word, black description.
   -------------------------------------------------------------------------- */
/* The comp scatters the three cards: near-square, each on its own angle,
   overlapping, with two dot-and-line connectors running through the gaps.
   Every number below is measured off the Figma frame (cards 2004:247/:248/:249,
   connectors 2004:266-:270 and :285) and expressed as a percentage of the
   group box, so the whole arrangement scales as one piece.

   Two deliberate departures. The comp ends the Orchestrate->Transform connector
   in mid-air, 27 units past Transform's right edge and 50 past its top, while
   its other three ends all sit inside their card. That end is moved onto the
   card so the pair reads as connected, at (1182.1, 522.2) — 14 degrees of line
   angle. And both connectors carry one elbow rather than running straight: a
   perpendicular offset of 15% of the line's length from its midpoint, the same
   rule dotcards.js applies to the stacked layout. The two bend to opposite sides
   so they do not read as parallel; the red one has only one choice, since the
   other side puts its bend inside the Discover card. Its first landing, (1195, 502), was inside the card's bounding box but
   only 1.6 units clear of the ROUNDED corner, so the ring straddled the border.
   The current point clears it by 24.6 and leaves the line running 42 units
   inside the card, both within a couple of units of the red Discover->
   Orchestrate connector's 23.5 and 37. Endpoints live in the SVG in index.html;
   move one and re-check those two numbers against the red pair.

   Group box: 1345.9 x 1059.4 in the 1728-wide frame. Cards are 473 x 488. */
.dot-cards {
  position: relative;
  width: 100%;
  max-width: var(--dot-cards--max);
  margin: 0 auto;
  aspect-ratio: 1345.9 / 1059.4;
}

.dot-links-stack { display: none; }   /* only below 992 — see the stack block */

.dot-links {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 3;                       /* the black dot sits on Orchestrate's corner */
  pointer-events: none;
  overflow: visible;
}

.dot-card {
  position: absolute;
  width: 35.14%;                    /* 473 / 1345.9 */
  aspect-ratio: 473 / 488;
  display: flex;
  background-color: var(--surface--page);
  border: 1.5px solid var(--ink);
  border-radius: 8.5%;
  transform: translate(-50%, -50%) rotate(var(--rot));
  /* Type is sized from the card, not the viewport, so the lockup inside holds
     its proportions whatever the card is doing. */
  container-type: inline-size;
}

/* The inset lives here, not on the card. A percentage padding on an absolutely
   positioned card resolves against the STAGE, not the card — 11% became 123px a
   side instead of 43px, and since cqw resolves against the container's content
   box it dragged every font size down with it. Inside the card, cqw is the
   card's own width, so the comp's proportions can be written straight out. */
.dot-card-inner {
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 100%;
  padding: 11cqw;
}

/* Centres and angles measured off the comp. Orchestrate sits behind the other
   two, which is the overlap order the comp draws. */
.dot-card--d { left: 20.54%; top: 60.96%; --rot: -8.4deg;  z-index: 2; }
.dot-card--o { left: 57.15%; top: 26.27%; --rot: 7.2deg;   z-index: 1; }
.dot-card--t { left: 76.94%; top: 72.04%; --rot: -12.4deg; z-index: 2; }

/* Sizes are the comp's, as a share of the card's own width: the letter is a
   graphic element at roughly a third of the card, not a heading. */
.dot-card-letter {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 900;
  font-size: 31.5cqw;
  line-height: .92;
}
.dot-card-word {
  margin-top: .04em;
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 800;
  font-size: 13.8cqw;
  line-height: 1.1;
}
.dot-card-body {
  margin-top: .9em;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: 5.8cqw;
  line-height: 1.35;
}

@media screen and (max-width: 991px) {
  /* Below the comp's width the scatter has nowhere to go — cards would be a
     third of a narrow viewport. Stack them instead, keeping a hint of the
     angles so the section still reads as the same idea. The comp's connectors
     are dropped with the scatter, since their coordinates describe it; the
     stack gets its own, measured off the rendered cards by dotcards.js. */
  .dot-cards {
    aspect-ratio: auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(22px, 4vw, 40px);
    max-width: 420px;
  }
  .dot-links { display: none; }         /* the comp's scattered geometry */
  .dot-links-stack {                    /* the stack's, drawn by dotcards.js */
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 3;
    pointer-events: none;
    overflow: visible;
  }
  .dot-card {
    position: static;
    width: 100%;
    aspect-ratio: auto;
    transform: rotate(var(--rot));
  }
  .dot-card-inner { padding: clamp(20px, 7cqw, 40px); }
  .dot-card--d { --rot: -3deg; }
  .dot-card--o { --rot: 2.5deg; }
  .dot-card--t { --rot: -3.5deg; }
  .dot-card-letter { font-size: clamp(40px, 16cqw, 74px); }
  .dot-card-word   { font-size: clamp(20px, 7cqw, 32px); }
  .dot-card-body   { font-size: clamp(16px, 3.6cqw, 18px); }  /* prose floor */
}

/* --------------------------------------------------------------------------
   11. Speakers
   Figma shows a constellation of red-ringed portraits wired into the dot
   network. Positions in that layout are derived from the real line-up, so
   until speakers are confirmed this ships the same card treatment in a
   responsive grid.
   -------------------------------------------------------------------------- */
/* Constellation strip. Without JS the track has no computed positions, so the
   nodes fall back to a plain wrapped row — the section still lists everyone. */
.speaker-constellation {
  position: relative;
  /* Full-bleed, like the comp: the constellation runs edge to edge while the
     section's heading and copy stay in the content column. Six photos at the
     comp's proportion (8.94% of the page) only reach a usable size at this
     width — inside the gutter they come out at 104px against the comp's 129,
     and the label pills lose ~35px each, which is where the titles were being
     sliced. Expressed in % of the parent rather than vw so a scrollbar can
     never turn it into horizontal overflow; capped at 1600 so the photos stop
     growing on a very wide screen. */
  width: calc(100% + 2 * var(--page--gutter));
  max-width: 1600px;
  margin-top: var(--space--lead);
  /* No centring of its own: the section is a flex column with
     align-items:center, which already centres an item wider than its content
     box, spilling it evenly into both gutters. Adding margin-left:50% on top of
     that stacks with the flex centring and threw the whole constellation 430px
     to the left. */
}

.speaker-strip {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;       /* one screenful of speakers per swipe */
  scrollbar-width: none;
  /* NO touch-action HERE. It used to read `pan-x`, with a comment claiming
     vertical swipes still scrolled the page — the opposite of what that value
     does. `pan-x` tells the browser this element handles horizontal panning
     ONLY, so a vertical drag starting anywhere on it scrolls NOTHING:
     measured with real touch events, the page moved 0px. On a phone the strip
     is full-bleed and tall, so the reader had to find the thin margin above or
     below it to get past. Default `auto` disambiguates by gesture direction —
     horizontal pages the strip, vertical scrolls the page — and unlike
     `pan-x pan-y` it also leaves pinch-zoom working. */
  overscroll-behavior-x: contain;      /* don't trigger back-navigation at the end */
  -webkit-overflow-scrolling: touch;
}
.speaker-strip::-webkit-scrollbar { display: none; }
.speaker-strip:focus-visible { outline: 2px solid var(--background--red-300); outline-offset: 4px; }

.speaker-track {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  justify-content: center;
}
.speaker-track.is-laid-out { flex-wrap: nowrap; gap: 0; justify-content: flex-start; }

/* One screenful. JS sizes it to the strip's client box and gives it its own
   network, so paging is exactly one page width. */
.speaker-page {
  position: relative;
  flex: 0 0 auto;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.speaker-links { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.speaker-track:not(.is-laid-out) .speaker-links { display: none; }

/* ---- node ------------------------------------------------------------- */
.speaker-node {
  position: relative;
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  width: var(--photo-size, 104px);
}
.speaker-page .speaker-node { position: absolute; }

.speaker-photo {
  display: block;
  width: var(--photo-size, 104px);
  height: var(--photo-size, 104px);
  margin: 0 auto;
  border: 2px solid var(--background--red-300);
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 22%;
  background-color: #f2f2f2;
  transition: border-width .15s ease, transform .18s ease;
}
/* Reference photos are full colour — greyscale here was carried over from the
   committee treatment by mistake. Hover therefore cannot be "desaturate ->
   colour"; it thickens the ring and lifts the photo slightly instead. */
.speaker-node:hover .speaker-photo,
.speaker-node:focus-visible .speaker-photo { border-width: 4px; transform: scale(1.05); }

/* Nodes that do not open a profile: a confirmed speaker whose blurb has not
   arrived, and the "to be announced" placeholders. They keep their place in the
   constellation — the comp's arrangement only reads as a network when the slots
   are filled — but must not offer the affordances of one that does. */
.speaker-node--static,
.speaker-node--tba { cursor: default; }
.speaker-node--static:hover .speaker-photo,
.speaker-node--tba:hover .speaker-photo { border-width: 2px; transform: none; }

/* An empty slot is drawn as a dashed ring, which is the page's own vocabulary:
   the hero decoration and the dot field are both full of open red circles. */
.speaker-photo--tba {
  border-style: dashed;
  background-color: transparent;
}
/* #767676 on white is 4.54:1, so it clears WCAG 2.2 AA for body text; a
   lighter grey would not, and this is running copy, not a disabled label. */
.speaker-node--tba .speaker-name { color: #767676; }
/* The pulse ring belongs to a real node on the network; an empty slot does not
   get to advertise itself. */
.speaker-node--tba::after { display: none; }
.speaker-node:focus-visible { outline: none; }
.speaker-node:focus-visible .speaker-photo { outline: 3px solid var(--ink); outline-offset: 3px; }

/* ---- pulse ring (the comp's concentric node) --------------------------- */
.speaker-node::after {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: var(--photo-size, 104px);
  height: var(--photo-size, 104px);
  margin-left: calc(var(--photo-size, 104px) / -2);
  border: 2px solid var(--background--red-300);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  animation: speaker-pulse 2.4s ease-out infinite;
  animation-delay: var(--pulse-delay, 0s);
}
@keyframes speaker-pulse {
  0%   { transform: scale(1);    opacity: .55; }
  70%  { transform: scale(1.45); opacity: 0; }
  100% { transform: scale(1.45); opacity: 0; }
}

/* ---- label card -------------------------------------------------------- */
/* The comp shows the name, role and company permanently beside every photo, on
   a #fcfcfc pill — not on hover.

   --card-max is written by JS as a fraction of the page width. It has to be a
   hard cap: the comp's labels are "CEO / Stripe", ours run to "Director,
   Government Digital Products (Platform Products)", and an unbounded pill walks
   straight into the next speaker. The full text is in the modal. */
.speaker-card {
  position: absolute;
  top: 50%;
  left: calc(100% - 10px);
  transform: translateY(-50%);
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: var(--card-max, 180px);
  max-width: var(--card-max, 180px);
  padding: 9px 10px 9px 17px;   /* left clears the photo the pill tucks under */
  background-color: var(--surface--cta);
  border-radius: 12px;
  text-align: left;
  pointer-events: none;
}
.speaker-name,
.speaker-title,
.speaker-org {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* The clamp is vertical; without this a single long word ("Government") is
     wider than the capped pill and gets sliced mid-letter instead of wrapping. */
  overflow-wrap: anywhere;
}
.speaker-name  { -webkit-line-clamp: 2; }
.speaker-title { -webkit-line-clamp: 2; }
.speaker-org   { -webkit-line-clamp: 1; }

.speaker-name {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 700;
  font-size: 14px;                /* 14px floor; the pill grew to suit */
  line-height: 1.25;
  letter-spacing: .03em;
  text-transform: uppercase;
}
.speaker-title,
.speaker-org {
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: 14px;                /* 14px floor */
  line-height: 1.35;
  opacity: .78;
}

.speaker-bio { display: none; }

/* Description for the strip (aria-describedby), not a visible line.
   The comp's "Explore all speakers →" sat here as visible copy, but that was
   the comp's ONLY affordance — it has no dots or arrows. With the pager
   underneath, the line repeated the controls, and its arrow read as a link to
   an all-speakers page that does not exist. What a screen-reader user actually
   needs is how to operate the strip, which is what this now says. */
.speaker-hint {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- pager: • • • under the strip -------------------------------------
   Not overlaid on the constellation: at full bleed the last speaker's label
   reaches 99% of the page, so anything pinned to the edge covers it. Keeping
   the controls out of the artwork is also closer to the comp, which has none.
   Dots only, on all three: the strip, the committee and the carousel. The
   committee used to carry prev/next buttons as well; they are gone, and the
   keyboard path they doubled as is Left/Right on the focused track. */
.speaker-pager,
.committee-pager,
.carousel-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-top: clamp(14px, 1.6vw, 24px);
}
.speaker-pager[hidden],
.committee-pager[hidden],
.carousel-pager[hidden] { display: none; }

/* ---- page dots (the comp's pagination component, 2004:289) ------------- */
.speaker-dots,
.committee-dots,
.carousel-dots {
  display: flex;
  justify-content: center;
  gap: 10px;
}
.speaker-dots[hidden] { display: none; }
.speaker-dot,
.committee-dot,
.carousel-dot {
  appearance: none;
  width: 12px;
  height: 12px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background-color: #d9d9d9;
  cursor: pointer;
  transition: background-color .16s ease;
}
.speaker-dot.is-current,
.committee-dot.is-current,
.carousel-dot.is-current { background-color: var(--background--red-300); }
.speaker-dot:focus-visible,
.committee-dot:focus-visible,
.carousel-dot:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

/* ---- below the six-across template the pill cannot sit beside the photo
       (four of them will not fit across 768px), so it becomes a caption under
       it. A pure width query now — the labels are no longer hover-dependent, so
       whether the device has a pointer is irrelevant. */
@media (max-width: 1199px) {
  .speaker-card {
    position: static;
    transform: none;
    min-width: 0;
    max-width: none;
    width: 100%;
    padding: 6px 2px 0;
    background: none;
    text-align: center;
    align-items: center;
  }
  .speaker-node { height: auto; }
  .speaker-page .speaker-node { height: auto; }
  .speaker-name {
    font-size: 14px;
    line-height: 1.2;
  }
  .speaker-title {
    font-size: 14px;
    line-height: 1.25;                  /* a 57-character job title must not
                                           push into the slot below */
  }
  .speaker-org { display: none; }        /* company lives in the modal */
}

/* ---- profile modal ----------------------------------------------------- */
.speaker-modal {
  /* Stated rather than inherited from the UA sheet, so a later `.body > *`-style
     rule can never quietly drop the modal back into document flow. */
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(760px, calc(100vw - 48px));
  max-width: none;
  max-height: min(86dvh, 700px);
  padding: 0;
  border: 2px solid var(--ink);
  border-radius: 18px;
  background-color: var(--surface--page);
  color: var(--ink);
  overflow: hidden;
}
.speaker-modal::backdrop { background: rgba(0, 0, 0, .55); }

.speaker-modal-inner {
  position: relative;
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: clamp(16px, 2vw, 30px);
  padding: clamp(20px, 2.4vw, 34px);
  max-height: inherit;
}

.speaker-modal-close {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  appearance: none;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
  z-index: 2;
}
.speaker-modal-close:hover { color: var(--background--red-300); }

.speaker-modal-photo {
  width: 100%;
  aspect-ratio: 1;
  border: 2px solid var(--background--red-300);
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 22%;
}

.speaker-modal-body { min-width: 0; display: flex; flex-direction: column; }

.speaker-modal-name {
  margin: 0 40px 0 0;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 900;
  font-size: clamp(22px, 2vw, 32px);
  line-height: 1.15;
}
.speaker-modal-role {
  margin: .35em 0 .9em;
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--small);
  line-height: 1.35;
}
/* Wheeling over a long bio must not chain to the page behind once it hits the
   end. The page is deliberately left scrollable while the dialog is open: the
   usual `body { overflow: hidden }` lock propagates to the viewport here (html
   is overflow:visible), which makes the document non-scrollable and clamps the
   reader's position to the top of the page. */
.speaker-modal-bio {
  overscroll-behavior: contain;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: 16px;
  line-height: 1.6;
}
.speaker-modal-bio p { margin: 0 0 1em; }
.speaker-modal-bio p:last-child { margin-bottom: 0; }

@media screen and (max-width: 767px) {
  .speaker-modal {
    width: 100vw;
    height: 100dvh;
    max-height: 100dvh;
    border: 0;
    border-radius: 0;
    margin: 0;
  }
  .speaker-modal-inner {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    height: 100dvh;
    grid-template-rows: auto auto auto 1fr;
    padding: 56px 20px 20px;
  }
  .speaker-modal-photo { width: 132px; }
  .speaker-modal-body { width: 100%; text-align: left; overflow: hidden; }
  .speaker-modal-name { margin-right: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .speaker-node::after { animation: none; }
  .speaker-photo { transition: none; }
  .speaker-strip { scroll-behavior: auto; }
}

/* Red text link with arrow (2004:317). */
.beacon-textlink {
  display: inline-block;
  margin-top: clamp(24px, 3vw, 52px);
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--lead);
  text-decoration: none;
}
.beacon-textlink:hover { text-decoration: underline; }

/* --------------------------------------------------------------------------
   12. Tickets — benefits card + tier card + BUY NOW (2004:283/:284/:305)
   -------------------------------------------------------------------------- */
.ticket-layout {
  display: grid;
  grid-template-columns: minmax(0, 508fr) minmax(0, 808fr);   /* comp widths */
  gap: clamp(18px, 3vw, 53px);
  /* stretch, not start: the two cards are a matched pair and must share a
     bottom edge. The tier card runs 58-124px taller than the benefits card
     across the two-column range, and `start` let each size to its own content.
     When the layout collapses to one column below 768px this has no effect —
     each card is then its own grid row. */
  align-items: stretch;
  max-width: var(--content--max);
  margin: var(--space--lead) auto 0;
}

.ticket-benefits,
.ticket-tiers {
  display: flex;
  flex-direction: column;
  padding: clamp(20px, 2.4vw, 42px);
  background-color: var(--surface--cta);
  border: var(--border--strong) solid var(--ink);
  border-radius: clamp(16px, 1.9vw, 32px);
}

/* The checklist is shorter than the tier list, so once stretched it has slack.
   Centring it reads as a deliberate short list in a tall card; pushing the items
   apart to fill would give 60px gaps between one-line bullets. */
.ticket-benefits { justify-content: center; }

.ticket-benefit {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .6em;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 700;
  font-size: var(--type--detail);
  line-height: 1.32;
}
.ticket-benefit + .ticket-benefit { margin-top: 1em; }
.ticket-benefit::before { content: "\2713"; font-weight: 400; }

.ticket-tier + .ticket-tier {
  margin-top: clamp(14px, 1.7vw, 29px);
  padding-top: clamp(14px, 1.7vw, 29px);
  border-top: 1px solid var(--content--grey-100);
}

.ticket-tier-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1em;
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 700;
  font-size: var(--type--detail);
}

.ticket-tier-info {
  margin-top: .5em;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: var(--type--detail);   /* a sentence, so the 16px prose floor */
  line-height: 1.4;
  opacity: .82;
}

/* auto keeps the CTA on the card's bottom edge if the benefits card is ever the
   taller of the two (narrow columns wrap the copy differently); the padding is
   the minimum separation from the last tier. */
.ticket-cta-row {
  margin-top: auto;
  padding-top: clamp(14px, 1.7vw, 29px);
}
.ticket-cta-row .beacon-cta { width: 100%; }

@media screen and (max-width: 767px) {
  .ticket-layout { grid-template-columns: 1fr; }
}

/* --------------------------------------------------------------------------
   13. Photo carousel (2004:286-:294)
   Reuses the Webflow slider: .w-slider-* markup and data attributes are the
   runtime's, only the paint changes.
   -------------------------------------------------------------------------- */
.beacon-carousel { position: relative; margin: clamp(40px, 5vw, 90px) 0; }

.beacon-carousel .slider {
  aspect-ratio: 1728 / 1122;
  width: 100%;
  height: auto;
  max-height: 78vh;
  background-image: none;
}

.beacon-carousel .w-slide { background-size: cover; background-position: 50%; }

/* The 2025 event photos. Own classes, NOT the legacy .slide-2/-5/-6 the markup
   used to carry: those rules live in the shared Webflow stylesheet, which the
   2023/2024/2025 archive pages still render from, so repointing them would have
   changed the photos on three other pages. (They also had .slide-5 and .slide-6
   on the same file, so the old three-slide carousel only ever showed two
   distinct pictures.) carousel.js counts Webflow's own generated dots, so going
   from three slides to five needs no JS change. */
.carousel-slide-1 { background-image: url("../images/beacon-2026-carousel-1.jpg"); }
.carousel-slide-2 { background-image: url("../images/beacon-2026-carousel-2.jpg"); }
.carousel-slide-3 { background-image: url("../images/beacon-2026-carousel-3.jpg"); }
.carousel-slide-4 { background-image: url("../images/beacon-2026-carousel-4.jpg"); }
.carousel-slide-5 { background-image: url("../images/beacon-2026-carousel-5.jpg"); }
.carousel-slide { background-repeat: no-repeat; }

.beacon-carousel .slider::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, .45);
  pointer-events: none;
  z-index: 1;
}

/* The headline is centred on the PHOTO, not on the section — the pager sits
   inside the section too, and centring against that would push the text down by
   half the pager's height. This wrapper is what the headline is absolute to. */
.carousel-stage { position: relative; }

.carousel-headline {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  /* 1280 / 1728 — the comp's own text box (2004:288), which is 13% in from each
     edge. It used to be a padding inset, which left the measure at 83% of a
     full-bleed photo: ~1900px and 135 characters a line at 2560. The cap holds
     the comp's absolute width once the viewport is wider than its frame.

     `inset: auto 0 0 0` with `top: 50%` also meant top AND bottom were set, so
     the box stretched from the half-way line to the foot of the photo and the
     translate lifted the text to a quarter of the way down instead of centring
     it. Positioning from the centre on both axes cannot do that. */
  width: 74.07%;
  max-width: 1280px;
  color: var(--ink--invert);
  font-family: Rubik, sans-serif;
  font-weight: 800;
  font-size: var(--type--carousel);
  line-height: 1.18;
  text-align: center;
  pointer-events: none;
}

/* The slider's own arrows and dot nav are gone: the photo carries no controls,
   and the pager below it (.carousel-pager, same chrome as the speakers strip)
   is the only affordance. display:none rather than a visual hide on purpose —
   Webflow gives both `role="button"` and `tabindex="0"` at runtime, so leaving
   them rendered would put a second, invisible set of controls in the tab order.
   carousel.js still drives them with .click(), which fires either way. */
.beacon-carousel .left-arrow-2,
.beacon-carousel .right-arrow-2,
.beacon-carousel .slide-nav { display: none; }

/* Sits on the white ground under the photo, not over it. Dots only — the
   carousel autoplays and wraps, so there is no end to walk to and nothing for
   an arrow to do that a dot or a swipe does not. */
.carousel-pager { margin-top: clamp(14px, 1.6vw, 24px); }

/* --------------------------------------------------------------------------
   14. Sponsors — tiered wall (2004:363/:364)
   -------------------------------------------------------------------------- */
.sponsor-tiers { max-width: var(--content--max); margin: var(--space--lead) auto 0; }
.sponsor-row + .sponsor-row { margin-top: clamp(24px, 3vw, 52px); }

.sponsor-tier-label {
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 700;
  font-size: var(--type--micro);
  letter-spacing: .08em;
  text-align: center;
  text-transform: uppercase;
}

.sponsor-slots {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  justify-content: center;
  gap: clamp(12px, 1.6vw, 28px);
  margin-top: clamp(10px, 1.2vw, 20px);
}

.sponsor-slot {
  flex: 0 1 clamp(120px, 14vw, 210px);
  aspect-ratio: 5 / 2;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed rgba(0, 0, 0, .2);
  border-radius: 10px;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 400;
  font-size: var(--type--micro);
  letter-spacing: .08em;
  opacity: .5;
}

/* A filled slot keeps the empty slot's box so tiers stay on a common grid, but
   drops the dashed placeholder outline. */
.sponsor-slot--logo {
  border: 0;
  opacity: 1;
  padding: clamp(6px, .8vw, 14px);
  transition: opacity .18s ease;
}
.sponsor-slot--logo:hover { opacity: .62; }

/* NOT .sponsor-logo: legacy already owns that name and gives it
   margin: 64px 0, which inflated every slot from 81px tall to 209px. */
.sponsor-mark {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* --------------------------------------------------------------------------
   15. Committee — portraits on red discs (2004:362)
   -------------------------------------------------------------------------- */
/* THE COMMITTEE IS A SLIDER AT EVERY WIDTH, and never wraps onto a second row.
   This element is the track; committee.js fills it with one .committee-page per
   screenful. Only the page's column count changes with the breakpoint — see
   .committee-page below, and PER_PAGE in committee.js, which must agree. */
.committee-grid {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
  /* NO touch-action HERE. It used to read `pan-x`, with a comment claiming
     vertical swipes still scrolled the page — the opposite of what that value
     does. `pan-x` tells the browser this element handles horizontal panning
     ONLY, so a vertical drag starting anywhere on it scrolls NOTHING:
     measured with real touch events, the page moved 0px. On a phone the strip
     is full-bleed and tall, so the reader had to find the thin margin above or
     below it to get past. Default `auto` disambiguates by gesture direction —
     horizontal pages the strip, vertical scrolls the page — and unlike
     `pan-x pan-y` it also leaves pinch-zoom working. */
  overscroll-behavior-x: contain;
  gap: clamp(22px, 2.6vw, 44px) 0;
  max-width: var(--content--max);
  margin: var(--space--lead) auto 0;
}
.committee-grid::-webkit-scrollbar { display: none; }
.committee-grid:focus-visible {
  outline: 2px solid var(--background--red-300);
  outline-offset: 4px;
}

.committee-member {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 0 clamp(8px, 1.2vw, 20px);
}
.committee-member + .committee-member { border-left: 1px solid var(--content--grey-100); }
.committee-member:nth-child(4n + 1) { border-left: 0; }

/* Figma sets cut-out portraits on a solid red disc. The 2025 photographs are
   ordinary opaque headshots with busy backgrounds, so the disc reads as a ring
   around a circular B&W portrait rather than showing through behind the person.
   Swapping in cut-out exports later needs no markup change. */
.committee-avatar {
  width: clamp(88px, 8.4vw, 132px);
  height: clamp(88px, 8.4vw, 132px);
  padding: 5px;
  border-radius: 50%;
  background-color: var(--background--red-300);
  box-sizing: border-box;
}

.committee-photo {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 22%;      /* headshots are top-weighted */
  filter: grayscale(1);
}

.committee-name {
  margin-top: .85em;
  color: var(--ink);
  font-family: Rubik, sans-serif;
  font-weight: 700;
  font-size: var(--type--detail);
  line-height: 1.25;
  /* Two lines are always reserved, so the ROLES below line up across a row.
     Names are the one thing here whose length nobody controls: in a row of six
     "Vincent Teyssier" and "Madeleine Koh" wrap where the other four do not,
     and without this their roles alone sat a line lower. Reserving the space
     costs a little air under the short names and buys a straight baseline at
     every breakpoint and any roster. 2 x the 1.25 line-height. */
  min-height: 2.5em;
  letter-spacing: .03em;
  text-transform: uppercase;
}

.committee-role {
  margin-top: .25em;
  color: var(--background--red-300);
  font-family: Rubik, sans-serif;
  font-weight: 500;
  font-size: var(--type--small);
  line-height: 1.3;
  letter-spacing: .03em;
  text-transform: uppercase;
  text-wrap: balance;
}

/* One page per screenful. Pages are real wrapper elements built by
   committee.js rather than a column-flow trick on a flat grid: column flow
   fills top-to-bottom, which put the two Co-leads diagonally opposite instead
   of side by side. A grid per page keeps left-to-right reading order.

   HOW MANY FIT: five in one row on desktop, four in one row on a tablet, and a
   2x2 of four on a phone — where two rows is the point, since one row of four
   at 390 would be 76px a column. Everything above 768 is a single row, because
   the roster wrapping onto a short second row is the thing the slider exists to
   avoid. committee.js owns the page SIZE and has to agree with these counts. */
.committee-page {
  flex: 0 0 100%;
  display: grid;
  grid-template-columns: repeat(4, 1fr);   /* 768-1199, one row */
  row-gap: clamp(22px, 2.6vw, 44px);
  align-content: start;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
/* One row, so every member but the first carries the divider. */
.committee-page .committee-member { border-left: 1px solid var(--content--grey-100); }
.committee-page .committee-member:first-child { border-left: 0; }

@media screen and (min-width: 1200px) {
  .committee-page { grid-template-columns: repeat(5, 1fr); }
}
@media screen and (max-width: 767px) {
  /* Two rows here, so the first-child rule above is wrong: the divider belongs
     between the columns, which is every SECOND member. */
  .committee-page { grid-template-columns: repeat(2, 1fr); }
  .committee-page .committee-member { border-left: 0; }
  .committee-page .committee-member:nth-child(2n) {
    border-left: 1px solid var(--content--grey-100);
  }
}

/* --------------------------------------------------------------------------
   16. Container normalisation
   Legacy pins several section wrappers to width:900px (and .div-block-speakers-copy
   to align-items:flex-start), which caps and left-shifts every section. The 2026
   layout is centred and full-width, with per-component max-widths instead.
   -------------------------------------------------------------------------- */
.grab-your-ticket-copy,
.div-block-speakers-copy,
.div-block-agenda-copy,
.div-block-connect-with-our-sponsors-copy,
.div-block-grab-your-ticket-copy,
.speaker-company-copy,
.comittee,
.agenda-copy,
.connect-with-our-sponsors-2025,
.highlights-2025 {
  width: 100%;
  max-width: none;
  margin-left: auto;
  margin-right: auto;
  align-items: center;
}

/* These carry huge legacy bottom padding (up to 300px) for the old layout. */
.speaker-company-copy,
.comittee {
  padding-bottom: var(--space--section);
}

/* Legacy also hangs a flat 60px under the ticket section alone, which put a
   bigger gap after it than after any other section. */
.grab-your-ticket-copy { margin-bottom: 0; }
.div-block-speakers-copy { padding-bottom: 0; }
.div-block-speakers-copy,
.div-block-agenda-copy,
.div-block-connect-with-our-sponsors-copy {
  padding-left: 0;
  padding-right: 0;
  margin-bottom: 0;
}

/* Six TBA speakers read better as 3x2 than as a 5+1 orphan row. */
@media screen and (min-width: 992px) {
  .speaker-grid { grid-template-columns: repeat(3, 1fr); }
}

/* New blocks sit inside legacy `display:flex; flex-flow:column; align-items:center`
   sections, so they shrink-wrap unless given an explicit width. */
.dot-cards,
.agenda-tabs,
.agenda-panels,
.speaker-grid,
.ticket-layout,
.sponsor-tiers,
.committee-grid { width: 100%; }

/* --------------------------------------------------------------------------
   17. Background dot field — DOT FIELD OFF [4 of 4]

   Fixed to the viewport rather than sized to the 7000px document, so the
   canvas costs one screen of pixels no matter how long the page gets.
   Content is lifted above it by the `.body > *` rule in section 2. The
   `transition: opacity .7s ease` below is the one the hero trace waits on
   finishing — that note is carried out here rather than left inline, because
   CSS has no line comments and no nesting: a block-comment wrapper ends at the
   FIRST inner terminator, which would have left the closing brace loose in the
   stylesheet.

   The whole block is commented out with the rest of the field. Nothing else
   about it is changed. To bring the field back, delete this block's comment
   markers and the three in index.html: the canvas at the top of <body>, the
   dotfield.js script tag, and the beacon:dotfield-ready gate in the reveal.
   public/assets/js/dotfield.js itself is untouched.

   DO NOT also remove `.body > *:not(dialog), .body > main > *` in section 2.
   It reads like canvas plumbing and is not: `.cover-2025` gets its
   `position: relative` from that rule and from nowhere else, and
   `.hero-decoration` is `position: absolute; inset: 0`, so dropping it
   reparents the entire hero decoration over the whole page.
   -------------------------------------------------------------------------- */
/*
#beacon-dotfield {
  position: fixed;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity .7s ease;
}
*/

/* --------------------------------------------------------------------------
   18. Hero decoration — the extracted Figma geometry
   The hero no longer uses the procedural canvas above; it uses the exact marks
   read out of comp node 2004:37 by scripts/extract-hero-dots.py, inlined into
   index.html between the hero-decoration markers. Inlined rather than an <img>
   so page CSS reaches the classes and the later animation pass can address a
   single dot by id.
   -------------------------------------------------------------------------- */
.hero-decoration {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;                 /* `slice` crops; without this it spills */
  pointer-events: none;
}
.hero-decoration-svg { display: block; width: 100%; height: 100%; }

/* `slice`, filling the hero: positions spread across the whole first screen,
   and the aspect difference is taken as a crop rather than as letterboxing.

   This was `meet` with a centred, aspect-ratio'd box, because under `slice` a
   zoom takes its overflow off the SIDES — where the design's densest columns
   are. That is no longer a risk: every element now carries
   vector-effect="non-scaling-stroke" and is sized by its stroke, so scaling the
   viewBox moves marks without growing them, and the crop at the hero's own
   aspect is only 27 design px per side at 1440. The base rule above (inset 0,
   100% x 100%) is all this variant needs. */

/* ---- the responsive layer: ONE artwork, ONE scale, a FOLD ----------------
   There is a single <svg> at every width, rendered with the AUTHORED viewBox
   and preserveAspectRatio="slice" at every width too — the master's own rule,
   unchanged from desktop down to 320.

   That is deliberate and it is the load-bearing part. `slice` scales by
   max(W/1728, H/972), and below about 1280 the hero is always taller than 16:9,
   so the H/972 term wins and keeps winning: 0.860 at 1440, 0.757 at 1280, 0.967
   at 768, 0.782 at 390. The artwork therefore already fills the hero's HEIGHT
   exactly at every narrow size, at a scale inside the range desktop itself
   occupies, and the three rows land at the comp's own proportions with nothing
   to distribute. The build before this one overrode that with a per-row --cy
   and paid 113px and 216px of empty band between the rows at 768x1024.

   What `slice` gets wrong on its own is the horizontal: at 390 it shows design
   x 615-1113, the middle of the artwork, which the comp leaves empty because
   that is where the type goes. So one operation is added below 1280 and one
   only — a FOLD. The left column of clusters slides right and the right column
   slides left until the master's outermost ink sits --deco-edge of a gutter
   inside the frame; the centre column does not move, because `slice` already
   centres it. Then each column is cropped to its own window, which is what
   stops the two outer thirds — about 500 design units each, wider than a phone
   — from sliding on top of one another.

   ONE TRANSLATE PER COLUMN, SHARED BY ITS THREE CLUSTERS. Nothing is scaled and
   nothing moves relative to anything else inside a column, so the comp's left
   grid is still its columns at x 46/126/194 at their own spacing, in order, at
   every width. What this replaced gave each of the eight clusters its own
   --cx/--cy/--ca/--cs, which is a recomposition: measured, a phone put 55% of
   its marks in the middle eight of twenty columns where the master puts 16%,
   and top-centre — squeezed to --cs .34 — became a knot of red accents under
   the header. --cs could not shrink a mark (a dot's size is a stroke width in
   CSS px, and does not scale) so compressing a cluster made its accents read
   larger relative to their spacing, which is why the phone looked like it had
   oversized red dots when nothing had been enlarged.

   THE PLACEMENT IS AUTHORED HERE AND APPLIED BY hero-decoration.js — see
   placeClusters() and applyFold() there. It cannot be a plain CSS transform:
   the fold is derived from the RENDERED scale, which only exists at runtime.
   CSS declares how much to fold; the JS converts it. Desktop declares nothing,
   the transform attribute is removed, and the clusters keep the identity the
   extractor emitted — verified as 0 differing pixels at 1440, 1920 and 2560. */
:root {
  /* One multiplier for every mark class. Not one per class: with the fold there
     is no cluster compression left to compensate for, so the comp's own size
     hierarchy carries through untouched, and the brief is explicit that the red
     accents must not be scaled independently of everything else. This is only a
     safety valve for a very short hero, where `slice`'s own scale drops. */
  --deco-dot-k: 1;
  --deco-line-k: 1;

  /* 0 = the master untouched; 1 = fully folded. The breakpoint lives HERE and
     nowhere else — hero-decoration.js reads this rather than carrying a width
     of its own, which is how two comments in it came to name 768 and 1280 while
     the stylesheet said 1279. */
  --deco-fold: 0;
  /* Where the master's outermost ink lands, as a fraction of the page gutter.
     Half a gutter is the same band the dot field's own edge column occupies,
     and it is what keeps the column clear of the type: on a phone the type
     fills the whole content column, so its padded box starts one gutter minus
     --deco-keep-min in from the screen edge (42 - 16 = 26px at 390) and a
     column at 21px clears it. Move this and re-check that clearance. */
  --deco-edge: .42;
  /* How far in from each edge that side's column may be drawn, as a fraction of
     the hero width; what is left between them is the centre window. NOT equal,
     because the master is not symmetric — its left grid packs three columns
     into the first 148 design units and its right spreads three over 185, and
     the fold reproduces those distances exactly. Scaled in JS by how much the
     fold has to absorb, so all three windows open to the whole frame at 1279
     and nothing jumps across the breakpoint. */
  --deco-win-l: .40;
  --deco-win-r: .45;
  /* A floor under `slice`'s own scale, for a hero short enough that the comp's
     spacing would close up faster than its marks do. 0 = off. */
  --deco-k-min: 0;

  --deco-keep-min: 8;               /* px of clear air around the type, floor */
}

/* Marks and connectors that land on the hero's type, taken out by
   hero-decoration.js. See the comment on applyKeepOut() there. Kept in CSS
   rather than as an inline style so the JS only ever ADDS an attribute and
   never rewrites one the extractor authored. */
.hero-decoration [data-keepout] { display: none; }
/* And marks that would be SLICED by the frame's edge. The frame is `slice` at
   every width now, so this is an absolute rule again — see noCrop(). */
.hero-decoration [data-clipped] { display: none; }
/* And marks their column's window does not reach — the crop half of the fold. */
.hero-decoration [data-folded] { display: none; }

/* ---- tablet and phone ---------------------------------------------------
   Two tiers, and almost nothing in them. The fold derives everything else from
   the rendered geometry, so a breakpoint only has to say THAT it folds, not
   where any individual group goes. */
@media screen and (max-width: 1279px) {
  :root { --deco-fold: 1; }
}

@media screen and (max-width: 767px) {
  :root {
    --deco-keep-min: 16;            /* .23 of a 42px gutter is 9.7 — too little */
    --deco-k-min: .55;
  }
  /* The comp's six longest connectors. Shortening them instead was tried and is
     worse: a connector drawn at a fraction of its length keeps its fixed end
     and loses the mark it points at, so the phone showed parallel red stubs
     floating in white space. Dropping them keeps every connector that still
     reads as a link between two marks. */
  .hero-decoration [data-long] { display: none; }
}

/* ---- small phone --------------------------------------------------------
   The one place a size multiplier is needed. `slice`'s scale is a function of
   the hero's HEIGHT, and a 320x568 device leaves a 484px hero: the scale falls
   to 0.498 against 0.757 at 1280, so the comp's own spacing closes up by a
   third while the marks — stroke-driven, fixed in CSS px — do not. Taking every
   class down together restores the ratio without touching the hierarchy. */
@media screen and (max-width: 479px) {
  :root { --deco-dot-k: .88; }
}

/* Above the decoration. `.cover-2025` is already a stacking context (section 2
   gives every `.body > *` position:relative and z-index:1), so these only have
   to beat the layer's z-index 0 — but they are stated rather than left to DOM
   order, because the decoration is the hero's first child and a later sibling
   with `position:relative` and no z-index would otherwise be the thing that
   happens to win. */
.cover-2025 > .hero-logo-stage,
.cover-2025 > .clock-countdown,
.cover-2025 > .text-and-buttons-wrapper-2025 { position: relative; z-index: 1; }

/* --hero-shift nudges the type block down from where centring puts it.

   RE-TUNED FOR THE v2 LOCKUP, and it is now a FIT constraint, not a
   compositional one. The lockup carries CONNECTING THE DOTs inside it, so at
   768 and up the block went from 21.5% of the stage width (wordmark 10.463% +
   a 5% gap + a 6.02% tagline) to 35.625% — 66% taller — while BEACON itself
   renders at exactly the size it always did. The old min(6.5vw, 14vh) pushed
   the CTA 6px OUT of the hero at 1280x720; swept across 768-2560, min(4vw,
   7.5vh) is the largest value that clears it everywhere, and its tightest
   measurement is 26px at 1280x720 — the same clearance the previous build was
   tuned to (27px, same size).

   The consequence, stated plainly because the number is still in CLAUDE.md:
   the mark's top edge now lands at 20.4-34.4% of the hero where the comp put a
   one-line wordmark at 32-43%. That band cannot be held by a block two thirds
   taller without pushing the CTA out, and the comp it came from composed the
   tagline as a separate element. Fit wins; do not restore the old value.

   Shifting by a fraction of the VIEWPORT WIDTH is what keeps it stable — the
   decoration's height comes from the width, so a vh- or px-based nudge would
   drift the moment the window changed shape. Capped against the height as
   well, so a short window shrinks the shift rather than pushing the CTA out.

   What this is NOT is collision avoidance. That was its second job while the
   shift had to find a decoration-free band; applyKeepOut() in
   hero-decoration.js does it properly now.

   BELOW 768 the stage goes full-bleed and the lockup fills the hero on its
   own, so the shift stays at zero there.

   `top` on already-relative children, not padding or margin: it moves the block
   without touching the flex centring the section relies on for short viewports,
   and without introducing a transform (which would create a stacking context
   around the hero logo's live SVG). */
.cover-2025 {
  --hero-shift: min(4vw, 7.5vh);
}
@media screen and (max-width: 767px) {
  .cover-2025 { --hero-shift: 0px; }
}
.cover-2025 > .hero-logo-stage,
.cover-2025 > .clock-countdown,
.cover-2025 > .text-and-buttons-wrapper-2025 { top: var(--hero-shift, 0px); }

/* --------------------------------------------------------------------------
   14. Accessibility / SEO scaffolding
   --------------------------------------------------------------------------
   Two elements that exist for assistive tech and crawlers, not for sighted
   readers: the hero's <h1> (the wordmark is an SVG, so the page had no heading
   at all above the section titles) and the skip link.
   -------------------------------------------------------------------------- */

/* Out of flow, so it cannot disturb .cover-2025's column layout — the hero's
   children are positioned by --hero-shift and an in-flow heading would push
   them. clip-path rather than `display:none`, which would hide it from the
   accessibility tree as well and defeat the point. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Off-screen until focused, then pinned to the top-left. Sits above the hero
   decoration and the nav, both of which carry z-index.
   Selected as `.body > .skip-link`, not `.skip-link`: section 3's
   `.body > *:not(dialog)` is more specific and would otherwise force it back to
   `position: relative`, leaving an in-flow, page-shifting line box at the top of
   the document. */
.body > .skip-link {
  position: absolute;
  top: 0; left: 0;
  z-index: 1000;
  transform: translateY(-120%);
  padding: 12px 20px;
  background: var(--background--red-300, #db0301);
  color: #fff;
  font-family: Rubik, sans-serif;
  font-size: 16px;
  font-weight: 600;
  text-decoration: none;
  border-radius: 0 0 8px 0;
}
.body > .skip-link:focus {
  transform: none;
  outline: 2px solid #000;
  outline-offset: 2px;
}
