/* ============================================================================
   site-pages.css — styling for the homepage and the talks page.

   Scoped entirely to `sh-` prefixed classes so it cannot leak into the live
   homepage, which loads the same base stylesheets but not this file.

   Colour + type come from the site's existing tokens rather than new values:
     --_colors---3  white      --_colors---5  #f7f7f7 (card surface)
     --_colors---6  #111 dark  --_colors---7  #737776 (charcoal / muted)
     --_colors---8  #dafa9c    Laygrotesk / Mesotrial / Geistmono
   ========================================================================== */

/* --- Nav -----------------------------------------------------------------
   The nav bar carries only d-pad-t/d-pad-b of 32, and those attributes scale
   with the viewport (32/1920 of it), so a 390px phone was getting about 6px
   top and bottom rather than the 20px the artboard asks for. The element has
   no m-pad attribute and the step scale has no 20, so it is set here.
   Kept in this stylesheet so the live homepage, which shares the same nav
   markup and builders-table.css, is untouched. The extra .underlay-nav is
   there to outrank the attribute rule, which sits in a later inline <style>.
   Both the blend and solid header layers match this selector, so the two
   stay in register. */
@media (max-width: 991px) {
  .underlay-nav .underlay-nav__container {
    padding-top: 20px;
    padding-bottom: 20px;
  }
}

/* --- Hero ----------------------------------------------------------------
   The design pairs the lime primary with an outlined secondary that routes to
   the talks page. The row wraps rather than shrinking the buttons on narrow
   screens, since both labels are long. */

/* Hero headline size, taken from the artboards' type settings rather than the
   site's H1 scale (which is tuned for the much shorter "MOVE IDEAS"):
     mobile 390 wide -> 40px      desktop 1920 -> 96px
   These were previously derived from the height of Figma's text box (62px and
   68px) over the 0.85 line height, giving 36px and 80px. That undershoots by
   roughly 15%: those boxes are trimmed to cap height, not to the full type
   size, so the box is never the em box. 10.26vw and 5vw land on the artboard
   sizes at 390 and 1920, and both pass through 62.4px at the 1248 breakpoint
   so the ramp stays continuous across it.
   Below 1248 the long second line cannot fit unbroken, so it wraps; from
   1248 up it is nowrap and scales with the viewport, which keeps the line's
   proportion of the page constant the way the artboard has it. */
.hero-wrap h1 {
  font-size: clamp(2rem, 10.26vw, 62.4px);
  line-height: 0.85;
  letter-spacing: -0.02em;
}

/* No gap between the lines. The artboard now sets both as one text block at
   0.85 leading with nothing between them, so the 0.85 line height above is
   the whole spacing story. h1 margins are already zeroed in
   builders-table.css, so this only has to cancel the adjacent-sibling rule. */
.hero-wrap h1 + h1 { margin-top: 0; }

@media (min-width: 1248px) {
  .hero-wrap h1 {
    font-size: min(5vw, 96px);
    white-space: nowrap;
  }
}

.hero-actions {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
}

/* Quiet outline button next to the lime primary: grey stroke on a solid dark
   fill, inverting to white on hover. border-color needs !important to beat the
   blanket `.button-nav2 { border: 1px solid #DAFA9C !important }` in this
   page's own <style> block, which otherwise strokes every nav button lime. */
.hero-actions__secondary.button-nav2 {
  color: var(--_colors---3);
  background-color: #1f1f1f;
  border-color: #737776 !important;
  text-decoration: none;
}

.hero-actions__secondary.button-nav2:hover {
  color: var(--_colors---4);
  background-color: #fff;
  border-color: #fff !important;
}

.hero-actions__secondary.button-nav2 .button-text { color: inherit; }

@media (max-width: 767px) {
  .hero-actions { flex-direction: column; align-items: stretch; }
}

/* Hero spacing the artboards ask for that the attribute step scale cannot
   express (it jumps 18 to 24, with no 20). The attribute rules live in an inline
   <style> further down the page, so they win ties on source order; the extra
   .hero-wrap on each selector takes them on specificity instead. The m-*
   attributes break at 991px, so these match that rather than the 767px used
   for the button stacking above.
   The mobile attributes (mmb, m-gap) are declared !important, which no amount
   of specificity beats on its own, so the rules answering those carry
   !important too and win on specificity within that tier. The desktop ones
   (dmb, d-gap, d-pad-*) are not important, so they need no such thing. */

/* Artboard puts 20px between the button group and the small print. Two rules
   stack to make that gap today: the group's own dmb/mmb margin-bottom, and a
   .hero-note margin-top set in the page head (24/32px). They are siblings in a
   flex column so the margins add rather than collapse, which was giving 56px
   on desktop and 42px on mobile. Zero the group's side and let the note own
   the gap, which is what the head comment intends. */
.hero-wrap .hero-actions { margin-bottom: 0 !important; }
.hero-wrap .hero-note { margin-top: 20px; }

@media (max-width: 991px) {
  /* 20px under the headline block, and between the stacked buttons. */
  .hero-wrap .hero-headings { margin-bottom: 20px !important; }
  .hero-wrap .hero-actions { gap: 20px !important; }
  /* Small print is 16px desktop / 14px mobile; text="normal" carries the
     desktop size and the 1.4 line height, so only mobile needs pulling down. */
  .hero-wrap .hero-note { font-size: 14px; }
}

/* 20px of button padding on desktop; mobile's 12px already matches. */
@media (min-width: 992px) {
  .hero-wrap .hero-actions .button-nav2 {
    padding-top: 20px;
    padding-bottom: 20px;
  }
}

/* --- Body copy leading ---------------------------------------------------
   text="small" carries a leading of 0.9, which is a display setting meant for
   tight headlines. On the card, list and note copy it is used for, that reads
   as cramped over multiple lines. Put those paragraphs on 1.4, matching the
   hero intro and the rest of the body copy, and leave their size alone.
   p[text="small"] outranks the bare [text="small"] rule in the page's own
   later <style> block on specificity, so no !important is needed. Elements
   other than paragraphs keep the tight leading, which is what it is for. */
p[text="small"] { line-height: 1.4; }

/* --- Button labels -------------------------------------------------------
   Every button on these pages labels itself with a .button-text div, and
   neither that class nor any of the button classes sets a size, so labels
   inherit body's 16px at every width. The artboards want 14px on mobile.
   Deliberately set here rather than on .button-text in builders-table.css,
   which the live homepage also loads. 991px is where the site's own m-*
   attributes switch over. */
@media (max-width: 991px) {
  .button-text { font-size: 14px; }
}

/* Section background/foreground pairing. Headings themselves now use the
   site's own text=/font=/text-color= attributes, matching the live homepage. */
.sh-light { color: var(--_colors---4); }
.sh-dark  { color: var(--_colors---3); }

/* --- Section rails -------------------------------------------------------
   The mono label strip that opens most sections: label left, statement
   centred, year right, with a hairline under it. Only the left label shows
   on mobile — the other two are decorative. */

/* Was a flex row with space-between, which pinned the middle and end labels
   to the row's own edges instead of the page grid. The judges and FAQs
   headers place their three labels on columns 1, 7 and 12, so mirror the page
   grid here and land on the same columns. The row sits in a .layout spanning
   the full width, so repeating .grid's own template and gap at each
   breakpoint keeps these in register with the content above and below. */
.sh-rail__row {
  display: grid;
  grid-template-columns: repeat(var(--_site-variables---columns), minmax(0px, 1fr));
  grid-column-gap: var(--_site-variables---design-gap);
  align-items: baseline;
  width: 100%;
  padding-bottom: 0.75rem;
  /* Matches the h-line dividers: --_colors---2 (#a9a9a9) at the 50% that
     d-opacity="0.5" resolves to. Was solid currentColor, i.e. #737776, which
     read considerably darker than every other divider on the page. */
  border-bottom: 1px solid #a9a9a980;
  color: var(--_colors---7);
}

/* --_colors---2 (#a9a9a9), matching the eyebrows in the grid-pattern section
   breaks (Our judges, Schedule, Faqs), which use text-color="2". These were on
   --_colors---7 (#737776), which read visibly darker than those, so the two
   kinds of section break did not agree. Every rail sits on a .sh-light section,
   so there is no dark-background case to keep the charcoal for. */
.sh-rail__label {
  grid-column: 1 / 7;
  font-family: Geistmono, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: var(--_colors---2);
}

.sh-rail__label--mid { grid-column: 7 / 12; }
.sh-rail__label--end { grid-column: 12 / -1; justify-self: end; }

.sh-dark .sh-rail__row,
.sh-dark .sh-rail__label { color: var(--_colors---7); }

/* .grid drops to 8 columns at tablet and 6 at mobile, so follow it down and
   hold the same proportions rather than overflowing the track count. */
@media (max-width: 991px) {
  .sh-rail__row {
    grid-template-columns: repeat(var(--_site-variables---columns-tablet), minmax(0px, 1fr));
    grid-column-gap: var(--_site-variables---design-gap-tablet);
  }
  .sh-rail__label { grid-column: 1 / 5; }
  .sh-rail__label--mid { grid-column: 5 / 8; }
  .sh-rail__label--end { grid-column: 8 / -1; }
}

@media (max-width: 767px) {
  .sh-rail__row {
    grid-template-columns: repeat(var(--_site-variables---columns-mobile), minmax(0px, 1fr));
    grid-column-gap: var(--_site-variables---design-gap-mobile);
  }
  .sh-rail__label { grid-column: 1 / -1; }
  .sh-rail__label--mid,
  .sh-rail__label--end { display: none; }
}

/* --- Shared card icon (lime square with the plus-cluster glyph) ----------- */

.sh-criteria__icon {
  /* Size is a variable because this glyph is shared with the reason cards,
     which have no artboard of their own, so only the judging criteria are
     scaled up to the 56px the design calls for. Dot and pitch derive from it,
     holding the artboard's proportions of a 6px dot on an 8px pitch (6px plus
     a 2px gap) inside a 56px tile at every size. */
  --icon: 40px;
  --dot: calc(var(--icon) * 6 / 56);
  --pitch: calc(var(--icon) * 8 / 56);
  flex: 0 0 auto;
  display: inline-block;
  width: var(--icon);
  height: var(--icon);
  border-radius: var(--_site-variables---desktop-radius);
  background-color: var(--_colors---8);
  /* Five-dot plus from the design. The previous version drew only the four
     outer dots and left the centre empty. Painted rather than shipped as an
     asset so it inherits crisp edges at any size. */
  background-image:
    linear-gradient(var(--_colors---4), var(--_colors---4)),
    linear-gradient(var(--_colors---4), var(--_colors---4)),
    linear-gradient(var(--_colors---4), var(--_colors---4)),
    linear-gradient(var(--_colors---4), var(--_colors---4)),
    linear-gradient(var(--_colors---4), var(--_colors---4));
  background-repeat: no-repeat;
  background-size:
    var(--dot) var(--dot), var(--dot) var(--dot), var(--dot) var(--dot),
    var(--dot) var(--dot), var(--dot) var(--dot);
  background-position:
    calc(50% - var(--pitch)) 50%,
    calc(50% + var(--pitch)) 50%,
    50% calc(50% - var(--pitch)),
    50% calc(50% + var(--pitch)),
    50% 50%;
}

/* --- 03 Judging criteria -------------------------------------------------- */

/* Left-hand intro column: heading, paragraph and the pass button.

   The artboard sets this heading at 72px, which falls between the h2 (120px)
   and h3 (96px) steps, so it is sized here rather than by a text= token. The
   0.85 leading text="2" already applies is what the artboard uses, so only the
   size is overridden. The floor keeps mobile on its current 48px so nothing
   below 1280 changes. The element selector outranks the [text="2"] rule in the
   page's own later <style> block. */
.sh-criteria-intro h2 {
  font-size: clamp(3rem, 3.75vw, 72px);
}

/* The artboard holds the copy to 710 of the column's 910, leaving 200px of air
   before the cards; as a percentage that proportion survives at any width.
   Only on desktop, since the two columns stack below 992. */
@media (min-width: 992px) {
  .sh-criteria-intro { padding-right: 22%; }
}

.sh-criteria {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Artboard card: 32px padding, 32px between the icon and the copy, 4px radius
   on #f7f7f7. Was 28px padding, 24px gap and square corners. */
.sh-criteria__item {
  display: flex;
  align-items: flex-start;
  gap: 32px;
  padding: 32px;
  border-radius: var(--_site-variables---desktop-radius);
  background: var(--_colors---5);
}

.sh-criteria .sh-criteria__icon { --icon: 56px; }

.sh-criteria__body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  flex: 1 1 0;
  min-width: 0;
}

/* Size, leading and weight come from text="9" on the element (32px desktop,
   22px mobile at line-height 1), which is exactly the artboard's H9. */
.sh-criteria__title {
  margin: 0;
  font-family: Laygrotesk, Arial, sans-serif;
  font-weight: 400;
}

/* text="9" also applies -0.03em tracking, which the artboard does not: it sets
   H9 at 0. The extra class outranks that attribute rule. */
.sh-criteria .sh-criteria__title { letter-spacing: normal; }

.sh-criteria__body p { margin: 0; }

/* On phones the cards stack full width, one below the other, rather than
   scrolling sideways. The mobile artboard shows a sideways scroller, but a
   stack was preferred: every card is readable without a horizontal gesture.
   Card internals still follow the artboard — icon above the copy, 20px
   padding and gaps, icon down to 40px. The list keeps the base
   flex-direction: column, so nothing needs setting for the stacking itself. */
@media (max-width: 767px) {
  .sh-criteria { gap: 16px; }

  .sh-criteria__item {
    flex-direction: column;
    gap: 20px;
    padding: 20px;
  }

  .sh-criteria .sh-criteria__icon { --icon: 40px; }
  .sh-criteria__body { gap: 12px; }
}

/* --- 04 Join as a team or individual -------------------------------------- */

.sh-join__card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-height: 26rem;
  padding: 1.75rem;
  overflow: hidden;
  color: var(--_colors---3);
  background: var(--_colors---6);
  border-radius: var(--_site-variables---desktop-radius);
  isolation: isolate;
}

/* Each card carries its own artwork, sized to cover. The dark fill underneath
   is what shows while the image is still decoding, so it is kept close to the
   art's own darkest tone rather than left transparent. */
.sh-join__card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
}

.sh-join__card--solo::before {
  background-color: #050505;
  background-image: url("../images/join-solo.avif");
}

.sh-join__card--team::before {
  background-color: #04120f;
  background-image: url("../images/join-team.avif");
}

/* Matches the audience cards on /talks-and-fireside (.st-audience__title
   / .st-audience__copy in talks.css, which this page does not load —
   keep the two in sync by hand). */
.sh-join__title {
  margin: 0 0 1rem;
  font-family: Mesotrial, Verdana, sans-serif;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.2vw, 2.5rem);
  line-height: 1.05;
  letter-spacing: -0.01em;
  text-transform: uppercase;
}

/* text="small" resolves to a line-height of 0.9 — below its own font-size —
   and its rule lives in a later inline <style>, so the class alone loses the
   tie. Qualify with the card to win on specificity, as the fireside page does. */
.sh-join__body p { margin: 0; max-width: 34ch; }
.sh-join .sh-join__body p {
  max-width: 36ch;
  line-height: 1.4;
}

/* The stage headings are authored as two lines with a <br>. At the copy
   column's width the longest of them ("Everyone starts") overruns by about
   8px and wraps to three, which shunts the paragraph down and makes the
   section jump between stages. Widening the column another notch is not
   available — d-end="7" would run the copy 70px under the image, which starts
   at column 7. Lend the heading the 40px gutter instead: it only borrows the
   empty space beside it, so the lines fit while long words still wrap rather
   than colliding with the image. */
@media (min-width: 992px) {
  #expectations [data-progress-heading] { margin-right: -2rem; }
}

/* ---- "How the hackathon works" progress bar ------------------------------
   Three stages here, against the live homepage's four. The segmented mobile
   mask in builders-table.css is hard-coded to 25% per segment and is shared
   with index.html, so override the step here rather than touching it. */
@media (max-width: 991px) {
  #expectations .progress-wrap {
    -webkit-mask-image: repeating-linear-gradient(
      to right,
      #000 0 calc(33.3333% - var(--seg-gap)),
      transparent calc(33.3333% - var(--seg-gap)) 33.3333%
    );
    mask-image: repeating-linear-gradient(
      to right,
      #000 0 calc(33.3333% - var(--seg-gap)),
      transparent calc(33.3333% - var(--seg-gap)) 33.3333%
    );
  }
}

/* button-nav2 is inline-block, so justify-content does nothing on it — the
   label has to be centred by text-align. */
.sh-join__cta {
  width: 100%;
  margin-top: 2rem;
  text-align: center;
}

.sh-join__cta .button-text { text-align: center; }

/* --- 05 Schedule ---------------------------------------------------------- */

/* Flex column so the slot list can fill the card and hand its slack to one
   row — see .sh-day__list below, which is what lines the two days' Lunch rows
   up with each other. */
.sh-day {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 1.75rem;
  border-radius: var(--_site-variables---desktop-radius);
  background: var(--_colors---5);
}

.sh-day__tag {
  /* .sh-day is a flex column (see above), which blockifies its children and
     stretches them to full width — that overrode the inline-block here and made
     the lime box span the card. align-self keeps it hugging its text. */
  align-self: flex-start;
  display: inline-block;
  margin-bottom: 1.5rem;
  padding: 0.2rem 0.5rem;
  font-family: Geistmono, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--_colors---4);
  background: var(--_colors---8);
}

.sh-day__title {
  margin: 0 0 0.25rem;
  font-family: Laygrotesk, Arial, sans-serif;
  font-weight: 400;
  font-size: 1.75rem;
  line-height: 1.1;
  letter-spacing: -0.01em;
}

.sh-day__note { margin: 0 0 1.5rem; }

.sh-day__list {
  display: flex;
  flex-direction: column;
  flex: 1;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Line the two days' Lunch rows up with each other. Both cards are the same
   height and both days have an identical run of rows after Lunch (five, one of
   them tall), so if all of a card's spare height goes into the one working
   block before Lunch, the Lunch rows land level. Day 1 has three rows before
   Lunch and day 2 only two, so day 2's Morning Build Session takes the larger
   share, which is what fills the open space there.
   Only the FIRST tall row may grow: splitting the slack between both tall rows
   leaves the Lunch rows half a row apart, so the sibling rule below cancels
   growth on any later one. */
.sh-day__list > .sh-slot--tall { flex-grow: 1; }
.sh-day__list > .sh-slot--tall ~ .sh-slot--tall { flex-grow: 0; }

.sh-slot {
  display: flex;
  align-items: flex-start;
  gap: 1.25rem;
  padding: 0.9rem 1rem;
  background: var(--_colors---3);
}

/* Longer working blocks read as taller rows in the design, which is what
   communicates "this is a long stretch" at a glance. */
.sh-slot--tall { padding-bottom: 3.75rem; }

/* Meals are the muted rows. */
.sh-slot--break { background: #e9e9e9; }

.sh-slot__time {
  flex: 0 0 3.5rem;
  font-variant-numeric: tabular-nums;
}

/* On phones the time moves above the session name so the name gets the card's
   full width instead of sharing it with a fixed left column. flex-basis has to
   be reset with it: in a column the basis sizes the main axis, so the time's
   `flex: 0 0 3.5rem` would set a 3.5rem height rather than a width. */
@media (max-width: 767px) {
  .sh-slot {
    flex-direction: column;
    /* the base rule's align-items: flex-start shrink-wraps children once the
       axis flips, which left the session name narrower than the row */
    align-items: stretch;
    /* 16px between time, session and speaker once they are stacked */
    gap: 16px;
  }

  .sh-slot__time { flex: 0 0 auto; }

  /* --tall's 3.75rem bottom padding and its flex-grow both exist to shape the
     desktop columns — the padding reads as "this is a long stretch", the grow
     lines the two days' Lunch rows up. Stacked on a phone the cards sit one
     above the other at content height, so neither does anything except open a
     60px hole inside the row. Back to the same padding as every other row.
     The grow reset needs the .sh-day__list ancestor to outrank the rule that
     sets it. */
  .sh-slot--tall { padding-bottom: 0.9rem; }

  .sh-day__list > .sh-slot--tall,
  .sh-day__list > .sh-slot--tall ~ .sh-slot--tall { flex-grow: 0; }
}

/* --- 06 Prize ------------------------------------------------------------- */

.sh-tiers { margin: 0; }

.sh-tiers__row {
  padding: 1.5rem 0;
  border-top: 1px solid rgba(0, 0, 0, 0.14);
}

.sh-tiers__row:first-child { border-top: 0; padding-top: 0; }

.sh-tiers__term {
  margin: 0 0 0.4rem;
  font-family: Laygrotesk, Arial, sans-serif;
  font-size: 1.375rem;
  line-height: 1.2;
  letter-spacing: -0.01em;
}

.sh-tiers__desc { margin: 0; }
.sh-tiers__desc p { margin: 0; max-width: 46ch; }

/* isolation makes this a stacking context, which is what lets the .dots layer
   show. .dots is z-index:-1, and without a stacking context here it painted
   behind this element's own background rather than on top of it — the hero and
   footer get away with it because their .dots sits over a parent that carries
   no background of its own. Inside a stacking context a negative-z child
   paints after the element's background and before its content, which is
   exactly the layer wanted. */
.sh-prize-collage {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  background: var(--_colors---6);
  border-radius: var(--_site-variables---desktop-radius);
  overflow: hidden;
  isolation: isolate;
}

/* The orbiting cards (prize-collage.js). Every card is absolutely centred and
   moved out to its place on the ring by a transform, so the ring itself needs
   no size of its own. */
.sh-prize-collage__ring {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.sh-prize-collage__card {
  position: absolute;
  left: 50%;
  top: 50%;
  overflow: hidden;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16);
  will-change: transform;
}

.sh-prize-collage__card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Above the ring — the cards orbit around the mark, not over it. Loaded as an
   <img> rather than inlined: animations.js identifies the footer's logo as the
   first <svg> on the page with a "0 0 359 82" viewBox, and a second inline
   copy this far up the document would take that identity from it. */
.sh-prize-collage__mark {
  position: relative;
  z-index: 1;
  display: block;
  width: clamp(7.5rem, 22%, 13rem);
  height: auto;
}

/* --- 07 Why they still join ---------------------------------------------- */

.sh-reason {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
  height: 100%;
  padding: 1.75rem;
  border-radius: var(--_site-variables---desktop-radius);
  background: var(--_colors---5);
}

.sh-reason__title {
  margin: 0;
  font-family: Laygrotesk, Arial, sans-serif;
  font-weight: 400;
  font-size: 1.375rem;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.sh-reason p { margin: 0; }

/* --- 08 Pricing ----------------------------------------------------------- */

.sh-plan {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 1.75rem;
  border-radius: var(--_site-variables---desktop-radius);
}

.sh-plan--dark {
  color: var(--_colors---3);
  background: var(--_colors---6);
}

.sh-plan--lime {
  color: var(--_colors---4);
  background: var(--_colors---8);
}

.sh-plan__tag {
  display: block;
  margin-bottom: 1.25rem;
  font-family: Geistmono, Arial, sans-serif;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* text="eyebrow" resolves to a light colour, which is invisible on the lime
   card. Both tags take the card's own foreground instead. */
.sh-plan--dark .sh-plan__tag { color: var(--_colors---3); }
.sh-plan--lime .sh-plan__tag { color: var(--_colors---4); }

/* Supporting copy on each card: #a9a9a9 on the black card, #191919 on the lime
   one. Grouped per variant rather than set with text-color= attributes, because
   the intro previously used text-color="7", whose rule is !important and would
   have needed fighting. #191919 is not one of the --_colors--- tokens; the
   nearest are black (--4) and #111 (--6), so it is written literally.
   The list colour goes on li, not the ul, since a rule on li would outrank it. */
.sh-plan--dark .sh-plan__intro,
.sh-plan--dark .sh-plan__unit,
.sh-plan--dark .sh-plan__meta,
.sh-plan--dark .sh-plan__list li { color: var(--_colors---2); }

.sh-plan--lime .sh-plan__intro,
.sh-plan--lime .sh-plan__unit,
.sh-plan--lime .sh-plan__meta,
.sh-plan--lime .sh-plan__list li { color: #191919; }

/* text="small" resolves to a line-height below its own font-size, which sets
   the intro copy solid. Restore the site's normal body rhythm. */
.sh-plan__intro {
  margin: 0 0 1.5rem;
  max-width: 46ch;
  font-size: 1rem;
  line-height: 1.4;
}

.sh-plan__price {
  margin: 0;
  font-family: Laygrotesk, Arial, sans-serif;
  font-size: clamp(2.75rem, 4vw, 4.5rem);
  line-height: 1;
  letter-spacing: -0.02em;
}

/* Size comes from text="xlarge" on the span, matching the body copy. It has to
   be set on the element because the price around it is ~58px. */
.sh-plan__unit {
  margin-left: 0.5rem;
  letter-spacing: 0;
}

.sh-plan__meta { margin: 0.5rem 0 1.5rem; }

.sh-plan__list {
  margin: 0 0 2rem;
  padding: 1.5rem 0 0;
  list-style: none;
  border-top: 1px solid currentColor;
}

/* No font-size here on purpose: a rule on li would outrank the size inherited
   from text="xlarge" on the ul, so the list would ignore it. */
.sh-plan__list li {
  position: relative;
  margin-bottom: 0.85rem;
  padding-left: 1.25rem;
  line-height: 1.4;
}

/* Square bullets, lime on the dark card and dark on the lime card. */
.sh-plan__list li::before {
  content: "";
  position: absolute;
  top: 0.5em;
  left: 0;
  width: 6px;
  height: 6px;
  background: currentColor;
}

.sh-plan--dark .sh-plan__list li::before { background: var(--_colors---8); }

.sh-plan__cta {
  width: 100%;
  margin-top: auto;
  text-align: center;
}

.sh-plan__cta .button-text { text-align: center; }

/* The pricing cards' buttons hold their colour on hover — no text or
   background change. There is deliberately no :hover rule here: the base
   .button-nav2:hover sets no colours of its own (only the variant classes
   ._1/._2/._3 do, and these buttons carry none of them), so leaving it alone
   is what keeps them static. */

/* The lime-background CTA needs a dark label. On the talks page this button is
   an <a> rather than a <div>, and builders-table.css carries
   `a:lang(en) { color: #fff }` at specificity 0,1,1 — which outranks the inline
   `.button-nav2 { color: #111 }` at 0,1,0. So the label came out white on lime
   there while the same button on the home page stayed dark. Two classes here
   beat both, and setting the label as well as the button means it cannot pick
   the white back up. No :hover variant, so this holds on hover too. */
.sh-plan__cta:not(.sh-plan__cta--invert),
.sh-plan__cta:not(.sh-plan__cta--invert) .button-text {
  color: var(--_colors---4);
}

/* The lime card's CTA is solid black — the paired-button treatment in the
   design, where each card's button is the other card's background. */
.sh-plan__cta--invert.button-nav2 {
  color: var(--_colors---3);
  background-color: var(--_colors---4);
  border-color: var(--_colors---4);
}

.sh-plan__cta--invert.button-nav2 .button-text { color: inherit; }

/* --- Judge carousel cursor -----------------------------------------------
   .spk-carousel advertises the drag with cursor: grab, but .spk-card sets
   cursor: pointer (added in 156a82f, "Speaker card is clickable") and the cards
   cover the whole track, so grab only ever showed in the gaps between them.
   Hand the drag cursor back across the cards and match the grabbing state while
   a drag is running; site.js toggles .is-dragging on the carousel. The cards
   stay clickable, a drag just reads as a drag now.
   Scoped here rather than dropping the pointer rule from builders-table.css,
   which the live homepage's own nine cards still use. */
.spk-carousel .spk-card { cursor: grab; }
.spk-carousel.is-dragging .spk-card { cursor: grabbing; }

/* --- Judge card portrait and logo ----------------------------------------
   The artboard rounds the portrait on all four corners, not just the two the
   card's own d-radius clips at the top. Same token the buttons read from, so
   the two stay in step. .spk-card__photo already has overflow: hidden, so the
   image and the logo below are both clipped to it.
   Set here rather than in builders-table.css because the live homepage carries
   nine of these cards and does not load this stylesheet; the talks page no
   longer uses them at all, so this reaches the judges only. */
.spk-card__photo {
  border-radius: var(--_site-variables---desktop-radius);
}

/* --- Judge portraits as particles -----------------------------------------
   judge-particles.js draws the portrait as stipple over the <img> and pushes
   the dots away from the cursor. It only ever adds .is-particles once its
   canvas actually holds a frame, so the plain <img> stays visible if the
   effect never starts — below 992px, under reduced motion, or if the source
   can't be decoded. */
.spk-card__particles {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* The canvas is opaque only where a dot lands, so the sharp photo underneath
   would show through the gaps and cancel the stipple. Hide it once the canvas
   is up — the canvas is drawn from the same image. */
.is-particles .spk-card__img,
.is-particles .spk-modal__photo { visibility: hidden; }

/* Whatever sits behind the canvas shows through those same gaps, so it sets
   the tone of the whole portrait. This section is white, which drains it; the
   modal figure and the fireside stack both already back theirs with black,
   which is why those read correctly. Match them. */
.spk-card__photo.is-particles { background: #000; }

/* The artboard sits each judge's company mark over the bottom-left of the
   portrait: 40px tall on a 24px inset, 28px on 20px at mobile.
   Decorative, because the role line beneath names the company. */
/* z-index clears the particle canvas, which sits over the photo at z-index 1.
   Without this the mark is in the DOM but painted underneath it, so it never
   appears. */
.spk-card__logo {
  position: absolute;
  z-index: 2;
  left: 24px;
  bottom: 24px;
  height: 40px;
  width: auto;
  pointer-events: none;
}

@media (max-width: 991px) {
  .spk-card__logo {
    left: 20px;
    bottom: 20px;
    height: 28px;
  }
}

/* --- Judge modal (split layout) -------------------------------------------
   The live component is a right-hand sheet. The design is a full-bleed 50/50
   split with the portrait on the left, so these rules only apply to the
   `--split` variant used on this page — the live homepage is unaffected. */

.spk-modal--split .spk-modal__figure {
  position: fixed;
  inset: 0 50% 0 0;
  z-index: 1;
  margin: 0;
  overflow: hidden;
  background: #000;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}

.spk-modal--split.is-open .spk-modal__figure { opacity: 1; }

.spk-modal--split .spk-modal__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The sheet becomes the right half rather than a floating panel. */
.spk-modal--split .spk-modal__sheet {
  width: 50%;
  max-width: none;
  left: auto;
  right: 0;
  border-radius: 0;
}

/* Desktop: the artboard floats both panels off the page edges rather than
   running them full-bleed. 8px of air on all four sides and 8px between the
   two, so each is (100vw - 8px * 3) / 2 = 50vw - 12px, which lands on the
   artboard's 948px at 1920. Corners use the same token as the buttons.
   Scoped to min-width because the mobile block below restacks them full-bleed,
   and a plain width here would fight its left/right insets. */
@media (min-width: 992px) {
  /* The portrait slides in from the left rather than fading, so it meets the
     sheet coming from the right in the middle of the screen. Off-screen start
     is its own width plus the 8px inset. Duration and easing deliberately copy
     .spk-modal__sheet in builders-table.css so the two halves travel together;
     if that transition is ever retuned, match it here too. Opacity is pinned on
     so only the transform reads — mobile keeps the original fade. */
  /* Both halves are revealed from the top rather than closing in from the
     sides — the two panels meeting in the middle read as a pair of doors.
     This masks rather than scales: scaleY squashed the portrait and stretched
     the type back into shape as it played. A clip inset moves the panel's
     visible edge down over contents that never move, so the face and the text
     are the right shape in every frame. */
  .spk-modal--split .spk-modal__figure {
    inset: 8px auto 8px 8px;
    width: calc(50vw - 12px);
    border-radius: var(--_site-variables---desktop-radius);
    opacity: 1;
    transform: none;
    clip-path: inset(0 0 100% 0 round var(--_site-variables---desktop-radius));
    transition: clip-path 760ms cubic-bezier(0.22, 1, 0.36, 1);
    transition-delay: 150ms;   /* closing: the face leaves last */
    will-change: clip-path;
  }

  .spk-modal--split.is-open .spk-modal__figure {
    clip-path: inset(0 0 0 0 round var(--_site-variables---desktop-radius));
    transition-delay: 0ms;     /* opening: the face arrives first */
  }

  /* The canvas is the portrait at this width, so the sharp <img> underneath it
     is never the thing on show — it is only a source for the stipple and a
     fallback. Keeping it hidden from the start is what lets the panel open
     immediately: there is no longer anything to race the reveal against, and no
     frame where the face appears before the dots do.

     is-photo-fallback is set by the modal if the canvas has not arrived within
     its grace period — a tainted canvas or a dead file — so the failure mode is
     the plain photo rather than an empty black panel. */
  .spk-modal--split .spk-modal__photo { visibility: hidden; }
  .spk-modal--split.is-photo-fallback .spk-modal__photo { visibility: visible; }

  /* Closed position has to clear the 8px inset as well as the panel's own
     width. The inherited translateX(100%) only covers the width, which left an
     8px strip of the sheet parked on screen down the right edge. The open state
     (.spk-modal.is-open .spk-modal__sheet) outranks this on specificity, so it
     still returns to translateX(0). */
  .spk-modal--split .spk-modal__sheet {
    top: 8px;
    right: 8px;
    bottom: 8px;
    left: auto;
    width: calc(50vw - 12px);
    border-radius: var(--_site-variables---desktop-radius);
    transform: none;
    clip-path: inset(0 0 100% 0 round var(--_site-variables---desktop-radius));
    transition: clip-path 760ms cubic-bezier(0.22, 1, 0.36, 1);
    transition-delay: 0ms;     /* closing: the text leaves first */
    will-change: clip-path;
  }

  /* 760ms on a quintic ease-out. The old 520ms on cubic-bezier(.7,0,.3,1) was
     an aggressive ease-in-out: it started hard, ran fast through the middle and
     braked late, which on a panel this tall read as a snap rather than a
     reveal. A long tail with no sharp entry is what makes it feel smooth.

     The stagger is mirrored rather than one-way: opening runs face then text,
     closing runs text then face. Each element carries a delay in one state and
     none in the other, so the sequence reverses with the direction instead of
     the pair snapping shut together. This file loads after builders-table.css,
     so `transform: none` above beats the translateX the base modal sets at the
     same specificity. */
  .spk-modal--split.is-open .spk-modal__sheet {
    clip-path: inset(0 0 0 0 round var(--_site-variables---desktop-radius));
    transition-delay: 150ms;   /* opening: the text follows the face */
  }
}

/* The talks modal heads its sheet with the company logo. The line-up only ships
   the grey #737776 file, so it is whitened here rather than duplicating every
   SVG — the same brightness/invert the mobile card badge uses. Sized off the
   optical --logo-scale so a mark that needed to be bigger in the list is bigger
   here too. */
.spk-modal__brand-logo {
  display: block;
  width: auto;
  height: calc(1.65rem * var(--logo-scale, 1));
  filter: brightness(0) invert(1);
}

/* The sheet is half a screen wide here and the name below runs enormous, so the
   mark needs more presence than the mobile column asks for. Only the base
   changes — the per-mark --logo-scale still does the optical balancing, so the
   whole set grows together and the weighting you tuned in the line-up holds. */
@media (min-width: 992px) {
  .spk-modal__brand-logo { height: calc(2rem * var(--logo-scale, 1)); }
}

.spk-modal--split .spk-modal__brand {
  margin: 0 0 3.5rem;
  font-family: Laygrotesk, Arial, sans-serif;
  font-size: 1.125rem;
  font-weight: 500;
  color: var(--_colors---3);
}

/* The artboard heads the sheet with the company logo rather than its name:
   48px tall on desktop, 32px at mobile. The fill script copies the src from
   whichever card was opened. */
.spk-modal--split .spk-modal__logo {
  display: block;
  height: 48px;
  width: auto;
  margin: 0 0 3.5rem;
}

@media (max-width: 991px) {
  .spk-modal--split .spk-modal__logo {
    height: 32px;
    margin-bottom: 2rem;
  }
}

.spk-modal--split .spk-modal__scroll { padding-top: 3rem; }

.spk-modal--split .spk-modal__role {
  margin-top: 1.25rem;
  color: var(--_colors---3);
}

.spk-modal--split .spk-modal__bio { margin-top: 2rem; }

/* Stack on mobile: copy first, portrait beneath it, matching the mobile
   artboard, which runs the name, role and bio down the page and finishes on the
   image. The 58/42 split keeps roughly the artboard's proportion of copy to
   portrait. Both halves swipe in from the right together: the sheet already
   does via translateX(100%) in builders-table.css, so the portrait is given the
   same transform and the same 520ms easing in place of its fade, and its
   opacity is pinned on so only the movement reads. */
@media (max-width: 991px) {
  /* One scrolling column rather than two viewport-pinned halves. Previously the
     copy scrolled inside its own 58vh box while the portrait stayed fixed to the
     bottom 42vh, which left a gap between them on a long bio. Now the sheet
     takes its natural height and the portrait follows directly beneath it, so
     the whole thing scrolls as one and a long bio simply pushes the image down.
     overflow-x is clipped because both halves start translated off to the
     right, which would otherwise make the column scrollable sideways. */
  .spk-modal--split {
    display: flex;
    flex-direction: column;
    overflow-x: hidden;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
  }

  /* The backdrop stays put instead of scrolling away with the column. */
  .spk-modal--split .spk-modal__overlay { position: fixed; }

  /* DOM order is figure then sheet, so order puts the copy first.
     overflow must come off hidden: it makes the sheet a scroll container, and a
     wheel or drag inside it is not chained past a non-scrollable overflow:hidden
     element to an ancestor, so the gesture died here and the column never
     scrolled. Nothing needs clipping at this size anyway, since the sheet is
     auto-height and square-cornered on mobile. */
  .spk-modal--split .spk-modal__sheet {
    order: 1;
    position: relative;
    inset: auto;
    flex: 0 0 auto;
    width: 100%;
    height: auto;
    overflow: visible;
  }

  /* The column scrolls now, so the sheet's inner scroller just grows. */
  .spk-modal--split .spk-modal__scroll {
    flex: 0 0 auto;
    overflow: visible;
  }

  /* 390 x 468 on the artboard, which is also the portraits' own 1160 x 1392. */
  .spk-modal--split .spk-modal__figure {
    order: 2;
    position: relative;
    inset: auto;
    flex: 0 0 auto;
    width: 100%;
    height: auto;
    aspect-ratio: 390 / 468;
    opacity: 1;
    transform: translateX(100%);
    transition: transform 520ms cubic-bezier(0.7, 0, 0.3, 1);
    will-change: transform;
  }

  .spk-modal--split.is-open .spk-modal__figure {
    transform: translateX(0);
  }

  .spk-modal--split .spk-modal__brand { margin-bottom: 2rem; }
}

/* .button-bg is an absolutely-positioned lime layer inside button-nav2, and at
   the mobile-full variant it paints over the button's own background — which
   left white text on lime. Match it to each button's intended fill. */
/* The variant rules set this with !important, so match that to override. */
.hero-actions__secondary.button-nav2 .button-bg { background-color: #1f1f1f !important; }
.hero-actions__secondary.button-nav2:hover .button-bg { background-color: #fff !important; }

.sh-plan__cta--invert.button-nav2 .button-bg { background-color: var(--_colors---4) !important; }

/* --- Attendee pass modal --------------------------------------------------
   Shares .form-modal with the hackathon sheet, so only the swap between the
   form and its success panel lives here. Both pages load this stylesheet. */

/* The form is showing until a submission lands; then it is replaced in place
   rather than the sheet re-laying out around a hidden element. */
.attendee-modal .attendee-success { display: none; }
.attendee-modal.is-done .attendee-form { display: none; }
.attendee-modal.is-done .attendee-success { display: block; }

.attendee-modal .attendee-fail {
  display: none;
  margin-top: 1rem;
  padding: 0.875rem 1rem;
  font-size: 0.9375rem;
  color: #7f1d1d;
  background: #fef2f2;
  border-radius: 0.5rem;
}

.attendee-modal .attendee-fail.is-shown { display: block; }

/* The day picker. Two checkboxes built from the same component as the terms
   row, which has no sibling and so carries no spacing of its own — stacked,
   they sat flush against each other. */
.attendee-days .radio_label + .radio_label {
  margin-top: 10px;
}


/* On mobile the close button sits at the top-right of the sheet, in the same
   band as the heading, and "GET YOUR ATTENDEE PASS" would run straight under
   it. The hackathon modal puts its heading and the button on one line, so
   this one does too: padding narrows the heading until it wraps clear of the
   button, rather than a margin dropping it below and leaving the button
   stranded above the content.

   5.5rem = the button's 3rem width plus its 2rem offset, and a little air.

   The success screen keeps the drop: it is a short centred message with
   nothing to wrap, so narrowing it would just make it ragged. */
@media (max-width: 991px) {
  .attendee-modal .attendee-form h1 { padding-right: 5.5rem; }
  .attendee-modal .attendee-success { margin-top: 3.5rem; }
}

/* The same collision, in the hackathon form's success screen. It used to open
   on "See you in the future, hacker." — long enough to wrap clear of the close
   button — and now opens on "Secure your place.", which fits on one line and
   runs straight under it. margin-top rather than padding: the close button is
   positioned against the sheet, so padding moves the text and leaves the box
   where it was. */
@media (max-width: 991px) {
  .hackathon-form + .success-message,
  .form-modal .success-message { margin-top: 3.5rem; }
}

/* The hackathon form restyles the shared checkbox and spaces its submit
   button with rules scoped to .hackathon-form, so neither reached this form —
   the box fell back to the component default (dark fill, lime border) and the
   button sat flush against the label. Same two values, same intent. */
.attendee-form .radio_label.checkbox {
  --_radios---border: #d9d9d9;
  --_radios---bg: #fff;
  --_radios---text: #fff;
}

.attendee-form .radio_label.checkbox:has(.radio_input:checked) {
  --_radios---border: #111;
  --_radios---bg: #111;
}

.attendee-form button[type="submit"] { margin-top: 28px; }

/* Honeypot. Off-screen rather than display:none — some bots skip fields they
   can tell are hidden, and none of this is reachable by keyboard or reader. */
.attendee-hp {
  position: absolute !important;
  left: -9999px !important;
  width: 1px !important;
  height: 1px !important;
  opacity: 0 !important;
  pointer-events: none !important;
}

/* The site's global anchor colour is white, for the dark sections it was
   written for. On this white sheet that renders the Terms and Privacy links
   invisible. The hackathon form answers this with its own rule scoped to
   .hackathon-form; this is the same rule for this form. */
.attendee-modal a {
  color: #111 !important;
  text-decoration: underline !important;
}

/* ---------------------------------------------------------------------------
   Modal close button, pinned

   On desktop, form.js and attendee-form.js move this button into the ASCII
   column. It cannot simply be restyled where the markup puts it:
   .form-modal-wrap is the scroll container AND carries the slide-up transform,
   and a transformed ancestor becomes the containing block even for
   position:fixed — so the button tracked the scroll wherever it sat inside.

   On mobile it is left exactly where the markup puts it, unchanged.
   --------------------------------------------------------------------------- */

/* The ASCII column is .height-screen (100vh) but the sheet it scrolls inside
   is 95vh — so the column was 28px taller than its own scrollport, and a
   sticky element taller than the scrollport cannot hold its position. That is
   the whole of the 28px drift the close button showed at the end of a scroll.
   Matching the two makes sticky hold exactly.

   Scoped to the modal so .form-sticky/.height-screen are untouched elsewhere,
   and paired with .form-modal-wrap's own height — if one changes, change both. */
.form-modal-wrap .form-sticky.height-screen {
  height: 95vh;
}

/* Desktop: inside the ASCII column. .form-sticky is position:sticky, so the
   button holds its place while the sheet scrolls under it, and it stays part
   of that block rather than floating in the viewport corner. Above the canvas,
   which is .absolute-cover at z-index auto. */
.form-sticky > .form-modal-toggle {
  position: absolute;
  inset: 2rem 2rem auto auto;
  z-index: 3;
}

/* Below 992px .form-sticky is hidden and the button goes back to exactly
   where the markup puts it, inside .form-modal-wrap — the original Webflow
   rule positions it there and the mobile behaviour is unchanged from before
   any of this. Nothing extra is needed here for that case. */
