/* WHYZ components. No colour literals - tokens only (standard 14).
   Layout primitives live in layout.css; this file is the furniture. */

/* ---------- header ---------- */
.masthead {
  /* axe reports color-contrast as INCOMPLETE for the nav links here, because the header
     is translucent over a photograph and it cannot resolve the background. Computed
     instead: at 88% --bg the effective background over the brightest possible pixel
     (pure white) is #2a2a2a, and --text-muted on #2a2a2a is 4.89:1 - above the 4.5
     threshold. Over the actual hero image's brightest strip it is 5.6:1. The
     translucency therefore cannot cause a contrast failure for any image. Raising the
     transparency past 12% would break that, so do not. */
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--border);
  padding-top: env(safe-area-inset-top);
}
.masthead__bar {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
  min-height: 52px;                 /* standard 24: < 15% of a 667px viewport */
}
.wordmark {
  font-family: var(--font-display); font-size: var(--fs-4); line-height: 1;
  letter-spacing: 0.04em; text-decoration: none; color: var(--text-strong);
  /* min-height alone left the target 39x44 - a tap target is two-dimensional. */
  min-height: var(--tap); min-width: var(--tap);
  display: inline-flex; align-items: center; justify-content: flex-start;
}
.nav { display: flex; gap: var(--sp-3); align-items: center; }
.nav a:not(.btn) {
  font-size: var(--fs-1); letter-spacing: var(--tracking-label); text-transform: uppercase;
  text-decoration: none; color: var(--text-muted);
  min-height: var(--tap); display: inline-flex; align-items: center;
  transition: color var(--dur) var(--ease);
  border-bottom: 2px solid transparent;
}
.nav a:not(.btn):hover { color: var(--accent); }
.nav a:not(.btn)[aria-current="page"] { color: var(--accent); border-bottom-color: var(--accent); }
.nav__links { display: none; gap: var(--sp-4); align-items: center; }
/* The one link that leaves the page is set apart from the four in-page anchors, so the
   row reads as "four places on this page, then somewhere else" rather than as five
   interchangeable links. */
.nav a.nav__page:not(.btn) {
  border-left: 1px solid var(--border);
  padding-left: var(--sp-4);
  color: var(--text);
}
.nav a.nav__page:not(.btn)::after { content: " \2192"; }
@media (min-width: 64rem) { .nav__links { display: flex; } }

/* The language switcher (docs/GERMAN-REVIEW-PANEL.md Task A). A single real <a> to the
   sibling-language page - no JS, no toggle state, no stored preference: site/privacy.html
   promises no cookies/localStorage/sessionStorage and this control must not make that
   false. It lives OUTSIDE .nav__links so it is visible at every width, not only the
   >=64rem link row above - the point is "always-visible", not buried in a menu. Its own
   border makes it read as "switch language" at a glance rather than as a sixth muted nav
   link, and min-width matches every other control on this bar (wordmark, menubtn): a tap
   target is two-dimensional, min-height alone is not enough (line 26 of this file). */
.nav a.nav__lang {
  border: 1px solid var(--border); border-radius: var(--radius-0);
  padding: 0 var(--sp-2); gap: var(--sp-1);
  min-width: var(--tap);
  margin-inline-start: var(--sp-2);
}
.nav a.nav__lang:hover { border-color: var(--accent); }
/* The code being switched TO reads brighter than the one already showing - "here, then
   there" - the same contrast .nav__page's arrow implies, without a second glyph. */
.nav__lang-tgt { color: var(--text-strong); }
.nav a.nav__lang:hover .nav__lang-tgt { color: var(--accent); }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: var(--tap); padding: var(--sp-2) var(--sp-4);
  font-family: var(--font-text); font-size: var(--fs-3);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  text-decoration: none; border: 1px solid transparent; border-radius: var(--radius-0);
  cursor: pointer; transition: transform var(--dur) var(--ease), background var(--dur) var(--ease);
  max-width: 100%; white-space: normal; text-align: center;   /* mobile100#96 */
}
.btn--primary { background: var(--field); color: var(--whyz-cream); }  /* 10.29:1 */
.btn--primary:hover { background: var(--field-deep); transform: translateY(-2px); }
.btn--ghost { background: transparent; color: var(--text); border-color: var(--border); }
.btn--ghost:hover { border-color: var(--accent); color: var(--accent); }
.invert .btn--ghost { color: var(--inv-text); border-color: var(--inv-border); }
.invert .btn--ghost:hover { border-color: var(--inv-accent); color: var(--inv-accent); }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; transform: none; }
/* mobile100 item 53 (defect 331fb45227): keyed to WIDTH alone, so a landscape phone
   (844x390 - wide, not narrow) never got the full-width treatment: measured 155px/18% of
   the viewport instead of the 358px/92% a portrait phone gets. 30rem/480px is the same
   short-viewport threshold the landscape menu and nav.js's askbar both already use. */
@media (max-width: 39.999rem), (max-height: 30rem) { .form .btn { width: 100%; } }

/* ---------- hero ----------
   Mobile and desktop are two different compositions, not one scaled.
   Mobile: full-bleed image first, wordmark below it, at its largest relative size.
   Desktop: text left across 7 columns, image half-bleeding to the right edge and
   crossing BOTH section boundaries so the layering is structural, not decorative. */
/* mobile100 item 28 (defect 511ca843b0): matches the .section rhythm cut in layout.css -
   an eighth of the desktop value (--sp-6, 128px) rather than a quarter, at mobile+tablet
   widths only; the min-width:64rem override below is untouched. */
.hero { padding-block: 0 var(--sp-4); }
.hero__text { padding-top: var(--sp-4); }
.hero__word {
  font-family: var(--font-display); font-size: var(--fs-6); line-height: 0.78;
  letter-spacing: -0.01em; color: var(--text-strong); margin: 0;
  transform: translateX(-0.06em);      /* optical alignment - COMPOSITION section 5 */
}
/* seo-geo-500#78: the home H1 states the topic ("WHYZ - Vienna's creative networking
   night"), not just the brand word other pages use ("Lost", "Imprint", "Vol. 01") - at
   the giant single-word wordmark size that text overflows its column on every viewport.
   Scoped to the home page only, so the wordmark treatment other H1s rely on is untouched. */
.hero__word--sentence { font-size: var(--fs-5); line-height: 0.92; transform: none; }
.hero__lede { font-size: var(--fs-4); max-width: 34ch; margin-block: var(--sp-3) var(--sp-4); }
.hero__ctas { display: flex; flex-wrap: wrap; gap: var(--sp-3); }
/* mobile100#21: the first screen carries one action, not two. "See the room" used to be a
   second full .btn beside "Join WHYZ"; it is demoted to quiet inline text so it stays
   reachable without competing with the one action the screen is for. */
.hero__secondary {
  min-height: var(--tap); display: inline-flex; align-items: center;
  margin-top: var(--sp-3);
  font-size: var(--fs-1); letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--text-muted); text-decoration: underline; text-underline-offset: 3px;
}
.hero__secondary:hover { color: var(--accent); }
.hero__media { position: relative; z-index: 2; }
.hero__media img { width: 100%; display: block; object-fit: cover; object-position: var(--pos, 50% 50%); }
.hero__stamp {
  position: absolute; left: 0; bottom: 0; z-index: 4;
  background: var(--field); color: var(--whyz-cream);   /* type on a FIELD, never on raw photo */
  font-size: var(--fs-1); letter-spacing: var(--tracking-label); text-transform: uppercase;
  padding: var(--sp-2) var(--sp-3); margin: 0; max-width: none;
}
/* mobile100 item 50 (defect e04dcf3154): a failed <img> left the browser's own broken-image
   glyph and unstyled alt text over a blank box - no caption, nothing that reads as
   deliberate. nav.js listens for the error event (it does not bubble, so that is a
   document-level capture-phase listener, not one per image) and hides the image, inserting
   one of these in its place. Same box language as .hero__stamp above: type on a FIELD,
   never on the grey the glyph would otherwise leave. */
.img-fallback {
  display: flex; align-items: center; min-height: 3rem;
  background: var(--field); color: var(--whyz-cream);
  font-size: var(--fs-1); letter-spacing: var(--tracking-label); text-transform: uppercase;
  padding: var(--sp-3); margin: 0; max-width: none;
}
@media (max-width: 63.999rem) {
  /* MOBILE COMPOSITION: the image comes FIRST, full-bleed, directly under the header,
     and the wordmark sits below it at its largest size relative to the viewport.
     Desktop is the opposite arrangement - text left, image half-bleeding right, both in
     one row. Same content, two compositions (BRIEF section 20). */
  .hero__media { grid-row: 1; }
  .hero__text  { grid-row: 2; padding-top: var(--sp-4); }
  /* mobile100 item 28 (defect 511ca843b0): 46vh -> 30vh, the least risky lever left after
     the seam and padding cuts, since shrinking content ABOVE the CTA row only ever
     increases the CTA's own clearance from the viewport bottom (item 8's floor), never
     reduces it - the two items do not compete here the way item 8's own note above
     suggests they might in general. min-height keeps the floor a very short phone would
     otherwise cross. */
  .hero__media img { aspect-ratio: auto; height: 30vh; min-height: 15rem; }
  /* Measured at 390x844: the primary CTA's own bottom edge landed 837.76px down in an
     844px-tall viewport - a 6.24px gap, under the 8px floor and inside the iOS
     home-indicator's gesture strip (mobile100 item 8). A margin AFTER the row does not
     move the row - only space ABOVE it does, so this trims the lede's own bottom margin
     on mobile instead of adding height below (which fights item 28's page-length ceiling
     instead of helping it). */
  .hero__lede { margin-bottom: var(--sp-3); }
}
@media (min-width: 64rem) {
  .hero { padding-block: 0 var(--sp-6); align-items: center; }
  /* Both items are explicitly in row 1. Without this the text lands in row 2, because
     grid auto-placement refuses to overlap an explicitly-placed item and the text and
     the media deliberately share column 7. Measured: text was landing at y=725. */
  .hero__text  { grid-row: 1; padding-top: var(--sp-5); padding-right: var(--sp-4); }
  .hero__media {
    grid-row: 1;
    margin-top: calc(var(--sp-5) * -1);      /* crosses up past the header edge */
    margin-bottom: calc(var(--sp-5) * -1);   /* and down into the band below */
  }
  /* Height is driven by the viewport, not by the crop ratio, so the wordmark stays
     above the fold. object-fit + the measured object-position keep the subject. */
  .hero__media img { aspect-ratio: auto; height: clamp(30rem, 76vh, 46rem); }
}

/* ---------- creed band: red as a FIELD, never as ink on dark ---------- */
.creed-band { background: var(--field); color: var(--whyz-cream); padding-block: var(--sp-4); position: relative; z-index: 1; }
.creed {
  text-wrap: balance;
  font-family: var(--font-display); font-size: var(--fs-5); line-height: 0.9;
  color: var(--whyz-cream); margin: 0;
}
.section__title {
  text-wrap: balance;
  font-family: var(--font-display); font-size: var(--fs-5); line-height: 0.9;
  color: var(--text-strong); margin: 0 0 var(--sp-3);
}
.invert .section__title { color: var(--inv-accent); }   /* 10.29:1 - red is legal ink on cream */

/* ---------- marquee ---------- */
.marquee { overflow: hidden; border-block: 1px solid var(--border); padding-block: var(--sp-2); }
.marquee__track {
  display: flex; gap: var(--sp-4); width: max-content;
  animation: marquee 38s linear infinite;
  font-family: var(--font-display); font-size: var(--fs-4); color: var(--text-muted);
  letter-spacing: 0.06em;
}
.marquee__track > span { white-space: nowrap; }
@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .marquee__track { animation: none; } }
/* mobile100#84: nothing moves on its own beside the primary action, and the track's
   animation ran the whole time it was in view (getAnimations() reported playState
   "running" for the full 38s loop). nav.js's own inline animationPlayState toggle (round 1,
   item 90 - pausing it once scrolled off-screen) is a different criterion and stays
   untouched; this just turns the animation off entirely below 64rem, where MOBILE-100
   applies and the desktop marquee is unaffected. */
@media (max-width: 63.999rem) { .marquee__track { animation: none; } }

/* ---------- forms ---------- */
.field { margin-bottom: var(--sp-3); }
.field label {
  display: block; font-size: var(--fs-1); letter-spacing: var(--tracking-label);
  text-transform: uppercase; color: var(--text-muted); margin-bottom: var(--sp-1);
}
.field input, .field select, .field textarea {
  width: 100%; min-height: var(--tap); padding: var(--sp-2);
  font-family: var(--font-text); font-size: var(--fs-3);   /* 16px - no iOS zoom */
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-0);
}
.field textarea { min-height: calc(var(--tap) * 2.5); resize: vertical; }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--accent); }
.field__hint { font-size: var(--fs-1); color: var(--text-muted); margin-top: var(--sp-1); }
/* mobile100 item 53 (defect f5637639dc): the collab form's submit button fell entirely
   below a covered-by-keyboard zone at 390x844 because this was the last field before it.
   A smaller bottom margin plus a shorter textarea here, on this one field only, trims the
   content above the button - genuine savings, but shaving pixels above a focused field can
   never guarantee reach against a keyboard whose height nothing here can know in advance.
   So below 64rem the button itself pins to the bottom of the visual viewport, the same
   fixed-and-safe-area pattern .askbar already uses, which keyboard-aware browsers keep
   above an on-screen keyboard rather than under it. */
.field--tight { margin-bottom: var(--sp-2); }
@media (max-width: 63.999rem) {
  .field--tight textarea { min-height: calc(var(--tap) * 2); }
  /* Sticky, not fixed: the button keeps its place in the form's own flow and its own
     .btn--primary background/contrast (10.29:1, untouched) - it only pins once scrolling
     would otherwise carry it past the bottom of the viewport. */
  #collab-form button[type="submit"] {
    position: sticky; bottom: calc(env(safe-area-inset-bottom) + var(--sp-2));
    z-index: 4;
  }
}
/* mobile100 item 22 (defect 2849f8c458): on a landscape phone the 390px-tall viewport is
   shorter than the sticky button's own resting offset plus the message field's height, so
   the two overlapped by a measured 52px - the fix for item 53 above traded a keyboard
   covering the button for the button covering the field it sits under. A short viewport is
   exactly the case a real on-screen keyboard would shrink further still, so pinning there
   helps nothing; falling back to the button's normal place in the form's flow is what the
   landscape menu (@media max-height: 30rem, above) and nav.js's askbar threshold already
   agree is "too short to float things". */
@media (max-height: 30rem) {
  #collab-form button[type="submit"] { position: static; }
}

/* The message that sits BESIDE the field it is about, written by forms.js the moment a
   field is refused. Empty until then, so it takes no space and announces nothing: a
   role="status" node with no text is silent, which is what lets it be created once and
   reused rather than inserted and removed on every attempt. Red is the brand FIELD colour
   and is 1.51:1 on the dark ground, so the error text uses the accent instead and the
   invalid state is carried by the border as well - never by colour alone. */
/* ---------- what people said ----------
   Nothing renders here until somebody has said yes, so every rule is written to disappear
   cleanly rather than to hold space: an empty set collapses, and the section leaves no gap
   behind it. The quote carries the weight and the name is small, because a testimonial
   that shouts is a testimonial nobody believes. */
.says__set:empty { display: none; }
.says__set { display: grid; gap: var(--sp-3); margin-block: var(--sp-4); }
@media (min-width: 48rem) { .says__set { grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); } }
.says { margin: 0; padding: var(--sp-3); border-left: 2px solid var(--field); background: var(--surface, transparent); }
.says blockquote { margin: 0; }
.says blockquote p { margin: 0; font-size: var(--fs-3); }
.says__who {
  display: block; margin-top: var(--sp-2);
  font-size: var(--fs-1); color: var(--text-muted); font-style: normal;
  letter-spacing: var(--tracking-label); text-transform: uppercase;
}
.says__who a:not(.btn) { color: inherit; }

/* Two ways to reach a person, as controls rather than as a sentence. 44px each and a real
   gap, because measured at 390 the prose version wrapped into two 24px targets touching at
   0px - the easiest mis-tap on the site. */
/* The no-JavaScript navigation. It only ever renders inside <noscript>, so it is never
   competing with the panel - and it is laid out as a plain row of links under the header
   rather than styled to look like the menu it replaces, because a visitor without
   JavaScript is better served by something obviously simple than by an imitation. */
.noscript-nav {
  display: flex; flex-wrap: wrap; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-3) 0;
}
.noscript-nav a:not(.btn) {
  min-height: var(--tap); display: inline-flex; align-items: center;
  color: var(--text); text-decoration: underline; text-underline-offset: 3px;
  letter-spacing: var(--tracking-label); text-transform: uppercase; font-size: var(--fs-1);
}
@media (min-width: 64rem) { .noscript-nav { display: none; } }   /* the header row is there */

.contact-list { list-style: none; margin: var(--sp-3) 0 0; padding: 0; display: grid; gap: var(--sp-3); }   /* 16px, the minimum clear space between two targets */
.contact-list li { min-width: 0; }   /* grid item, same min-width:auto trap as .footer__grid > * */
.contact-list a:not(.btn) {
  min-height: var(--tap); display: inline-flex; align-items: center;
  color: var(--text); text-decoration: underline; text-underline-offset: 3px;
  max-width: 100%;   /* inline-flex shrink-wraps to its unwrapped text otherwise - mobile100#96 */
}

.field__error:empty { display: none; }
.field__error {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-1); color: var(--danger);
  letter-spacing: var(--tracking-label);
}
[aria-invalid="true"] { border-color: var(--danger); box-shadow: inset 0 -2px 0 var(--danger); }
.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.form { max-width: 34rem; }
.form__status[data-state="error"] { color: var(--danger); }
.form__status[data-state="ok"] { color: var(--success); }

/* ---------- footer ---------- */
/* The last section's bottom padding and the footer's top padding stack at the seam, and
   on a phone they were 48px and 65px - a 113px run of nothing between the last button on
   the page and the first line of the footer, over the 96px a dead run is allowed. The
   boundary still wants air; it does not want both halves at full size. The desktop
   value is unchanged, where the page is wide enough to carry it. */
.footer { border-top: 1px solid var(--border); padding-block: var(--sp-4); }
@media (min-width: 64rem) { .footer { padding-block: var(--sp-5); } }
/* min-width: 0 is load-bearing. A grid item defaults to min-width:auto, which means it
   refuses to become narrower than its own content - so at 200% text zoom the first column
   measured 470px inside a 390px viewport and pushed the whole document to 502px wide,
   112px of horizontal scroll on a page that has none at any normal size. Zooming text is
   the one accessibility setting that a layout silently fails, because nobody browsing at
   100% ever sees it. */
.footer__grid { display: grid; gap: var(--sp-4); }
.footer__grid > * { min-width: 0; }
.footer__grid :is(p, a, li) { overflow-wrap: anywhere; }
@media (min-width: 64rem) { .footer__grid { grid-template-columns: 2fr 1fr 1fr; } }
/* :not(.btn) is load-bearing. `.footer a` is (0,1,1) and `.btn--primary` is (0,1,0), so
   without it the footer's JOIN button renders in --text-muted on the red field: 4.38:1,
   measured by axe. The identical bug shipped in .nav a earlier in the same session. */
.footer a:not(.btn) {
  text-decoration: none; color: var(--text-muted);
  min-height: var(--tap); display: inline-flex; align-items: center;   /* standard 22 */
}
.footer a:not(.btn):hover { color: var(--accent); }
.footer li { list-style: none; }
.footer ul { padding: 0; margin: 0; display: grid; gap: var(--sp-3); }  /* 16px between tap targets - 8px is the floor, not the target */
.footer p.wordmark { min-height: 0; display: block; }
/* The bottom-right is the terminal area of the Gutenberg diagram - the last place the eye
   rests. It held legal links; it now holds the single action, with legal moved into the
   navigation column where it is still one click away. */
.footer__end .btn { margin-top: var(--sp-3); }   /* 16px - 8px is the floor, not the target */
@media (min-width: 64rem) { .footer__end { text-align: right; } .footer__end .btn { margin-left: auto; } }
/* Below 64rem, the phone menu already reaches every one of these (item 18) and the ask
   bar already carries the join CTA - so hide the duplicates rather than scroll past them
   again. Measured before: footer 987px = 1.17 screens, all of it non-collapsible
   (mobile100 item 28). */
@media (max-width: 63.999rem) {
  .footer__menu-dupe, .footer__join { display: none; }
}

/* ---------- inverted sections ----------
   Muted greys tuned for a dark ground collapse on cream: --text-muted is 2.35:1 there,
   --inv-muted is 5.96:1. Measured, not assumed. */
.invert .field label,
.invert .field__hint,
.invert .footer a:not(.btn) { color: var(--inv-muted); }
.invert .contact-list a:not(.btn) { color: var(--inv-text); }
.invert .field input,
.invert .field select,
.invert .field textarea {
  background: var(--whyz-white); color: var(--inv-text); border-color: var(--inv-border);
}
.invert .field input:focus,
.invert .field select:focus,
.invert .field textarea:focus { border-color: var(--inv-accent); }
.invert .form__status[data-state="ok"] { color: var(--inv-success); }
.invert .form__status[data-state="error"] { color: var(--inv-danger); }
.invert .btn--primary { background: var(--field); color: var(--whyz-cream); }

/* ---------- small pieces ---------- */
.meta { font-size: var(--fs-1); color: var(--text-muted); letter-spacing: var(--tracking-label); text-transform: uppercase; }
.invert .meta { color: var(--inv-muted); }
/* mobile100#36: the footer tagline is BRAND-FACTS.md's locked wording ("Create. Connect.
   Collect.", three words exactly as written) and cannot be shortened to fit the 24-char
   letter-spaced-label ceiling, so it is exempted from that treatment here instead. */
.meta--plain { letter-spacing: normal; text-transform: none; }
.lede { font-size: var(--fs-4); max-width: 42ch; text-wrap: balance; }
/* A quiet aside. The three-line negative used to be the largest heading on the page;
   it is true but it is not the message, so it sits here at label scale instead. */
.aside {
  font-family: var(--font-text); font-size: var(--fs-2);
  color: var(--text-muted); border-left: 2px solid var(--border);
  padding-left: var(--sp-3); margin-top: var(--sp-4);
}
.invert .aside { color: var(--inv-muted); border-color: var(--inv-border); }
.notice { border-left: 2px solid var(--accent); padding-left: var(--sp-3); }
.invert .notice { border-color: var(--inv-accent); }

/* ---------- founder overview (admin.html, PROMPT-V15) ----------
   Reuses the tokens and rhythm of everything else on this page rather than inventing a
   new visual language for an internal tool - Ema, this session: "reuse the look". */
.overview__stats { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
.overview__stats li { border-top: 1px solid var(--border); padding-top: var(--sp-2); }
.overview__stats strong { display: block; font-family: var(--font-display); font-weight: 400; font-size: var(--fs-4); color: var(--text-strong); }
.overview__stats span { font-size: var(--fs-1); color: var(--text-muted); letter-spacing: var(--tracking-label); text-transform: uppercase; }

.overview__section { margin-top: var(--sp-4); padding-top: var(--sp-3); border-top: 1px solid var(--border); }

/* The 30-day chart. Plain inline SVG built by admin.js, not a library: the CSP here is
   script-src 'self' with no external origin allowed, and this is the one dashboard number
   that benefits from a shape rather than just a list of 30 numbers. */
.overview__chart-svg { width: 100%; height: auto; }
.overview__bar-views { fill: var(--accent); }
.overview__bar-signups { fill: var(--field); }
.overview__chart-legend { display: flex; gap: var(--sp-4); font-size: var(--fs-1); color: var(--text-muted); margin-top: var(--sp-2); }
.overview__chart-legend span { display: inline-flex; align-items: center; gap: var(--sp-1); }
.overview__chart-legend i { width: 0.7em; height: 0.7em; display: inline-block; }
.overview__chart-legend .overview__bar-views { display: inline-block; }
.overview__chart-legend .overview__bar-signups { display: inline-block; }

.overview__rates, .overview__env { list-style: none; margin: var(--sp-2) 0 0; padding: 0; display: grid; gap: var(--sp-2); font-size: var(--fs-2); }
.overview__rates li, .overview__env li { display: flex; align-items: center; gap: var(--sp-2); }
/* A dot rather than a colour on text: --danger and --success both fail as body text at
   this size for one of the two themes on this page, and a status light does not need to. */
.overview__dot { width: 0.6em; height: 0.6em; border-radius: 50%; flex: none; background: var(--danger); }
.overview__dot[data-ok="true"] { background: var(--success); }

/* ---------- founder view table (admin.html) ---------- */
.table-wrap { overflow-x: auto; }        /* wide tables scroll inside themselves, never the page */
.data { border-collapse: collapse; width: 100%; font-size: var(--fs-2); }
.data caption { text-align: left; color: var(--text-muted); padding: var(--sp-3) 0; }
.data th, .data td {
  text-align: left; padding: var(--sp-2); border-bottom: 1px solid var(--border);
  vertical-align: top; white-space: nowrap;
}
.data th {
  font-family: var(--font-text); font-size: var(--fs-1); text-transform: uppercase;
  letter-spacing: var(--tracking-label); color: var(--accent);
}

/* A second, lower-friction door next to the form. Deliberately a ghost button: the form
   is still the primary action, and two filled buttons would make the reader choose. */
.or-line {
  font-family: var(--font-text); font-size: var(--fs-1);
  text-transform: uppercase; letter-spacing: var(--tracking-label);
  color: var(--text-muted); margin-top: var(--sp-4);
}
.invert .or-line { color: var(--inv-muted); }

/* ---------- THE COLLAB CARD - the signature move ----------
   At Vol. 01 everyone filled a printed card at the door - name, what you do, one thing
   you're looking for - and pinned it to a board. That card is the event's own mechanism
   for the thing its guests are most afraid of: walking in knowing nobody.

   So the join form IS that card. Card stock, ruled lines, a stamped wordmark, pinned
   slightly off-square. It appears exactly twice on the site: here, and as the photograph
   of the real cards on /vol01. Never as decoration.

   Accessibility is not traded for it: every line is a real <label> + <input>, the inputs
   are 44px tall and 16px, and the rotation is removed under reduced motion and on mobile
   where it would cost horizontal space. */
.cardform {
  position: relative;
  background: var(--whyz-white);
  color: var(--inv-text);
  /* The card carries a light ground inside a dark section, so it must re-derive its own
     focus colour exactly as .invert does. Gold would be 1.4:1 on white card stock. */
  --focus: var(--inv-focus);
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  max-width: 34rem;
  /* A hard offset in the brand red, because a black shadow vanishes on a black ground.
     This is the card sitting ON the page, not floating above it. */
  box-shadow: 12px 12px 0 0 var(--field);
  border: 0;
}
@media (min-width: 64rem) { .cardform { transform: rotate(-1.2deg); } }
.cardform__stamp {
  font-family: var(--font-display); font-size: var(--fs-4); line-height: 1;
  letter-spacing: 0.06em; color: var(--inv-accent); margin: 0;
}
.cardform__kicker {
  font-family: var(--font-text); font-size: var(--fs-1);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--inv-muted); margin: 0 0 var(--sp-4);
}
/* a ruled line, not a box - this is the whole visual idea */
.cardline { margin-bottom: var(--sp-4); }
.cardline label {
  display: block; font-family: var(--font-text); font-size: var(--fs-1);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  color: var(--inv-muted); margin-bottom: var(--sp-1);
}
.cardline input {
  width: 100%; min-height: var(--tap);
  font-family: var(--font-text); font-size: var(--fs-3);
  color: var(--inv-text); background: transparent;
  border: 0; border-bottom: 2px solid var(--inv-text);
  border-radius: var(--radius-0); padding: var(--sp-2) 0;
}
.cardline input::placeholder { color: var(--inv-muted); opacity: 1; }
.cardline input:focus { border-bottom-color: var(--inv-accent); }
.cardform .field__hint { color: var(--inv-muted); }
.cardform .form__status[data-state="ok"] { color: var(--inv-success); }
.cardform .form__status[data-state="error"] { color: var(--inv-danger); }
.cardform .btn { margin-top: var(--sp-2); }
@media (max-width: 39.999rem) {
  /* "100% of .cardform" was still only 59% of the viewport at 390 (measured 230px), because
     it is 100% of a box already narrowed by .cardframe's own 32px padding plus .cardform's
     own 32px padding on each side. Bled past both, back to the shell's own margin, so the
     primary submit button reaches the width every other full-width control on the page
     does (mobile100 item 53: "full width and inside thumb reach"). */
  .cardform .btn {
    /* A <button> is a form control, not a plain block box: even with display:flex,
       width:auto shrinks it to its own content instead of filling the available space the
       negative margins open up - measured, that still shipped at 275px (70.6%) instead of
       reaching the shell edge like every other full-width control (item 53 needs 100%).
       An explicit calc() sidesteps the shrink-to-fit quirk: 100% of .cardform's own
       230px content box, plus back the 128px of .cardform + .cardframe padding the
       negative margin below opens up on both sides. */
    display: flex; width: calc(100% + var(--sp-4) * 4); margin-inline: calc(var(--sp-4) * -2);
  }
}
@media (prefers-reduced-motion: reduce) { .cardform { transform: none; } }

/* ---------- proof beside the ask ----------
   Item 19 of the GODMODE scale scored 1 for a measurable reason: the nearest thing to
   evidence sat 4285px above the form. Photographs prove an event happened, not that it
   worked, and there are no testimonials yet - so this block states the three things
   about Vol. 01 that are true without inventing anyone's words, at the point of decision.
   The numbers are set in the display face so they read as facts, not as decoration. */
.proof {
  display: grid; gap: var(--sp-3);
  margin-block: var(--sp-4);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.proof__shot { margin: 0; }
.proof__shot figcaption {
  font-size: var(--fs-1); color: var(--text-muted);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  margin-top: var(--sp-2);
}
.proof__facts { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-3); }
.proof__facts li { display: grid; grid-template-columns: 4.5rem 1fr; gap: var(--sp-3); align-items: baseline; }
.proof__facts strong {
  font-family: var(--font-display); font-weight: 400;
  font-size: var(--fs-4); color: var(--accent);
  letter-spacing: var(--tracking-display);
}
.proof__facts span { font-size: var(--fs-2); color: var(--text-muted); }
@media (min-width: 64rem) {
  .proof { grid-template-columns: 1fr 1fr; gap: var(--sp-4); align-items: start; }
}

/* The four "ways in" headings computed to 18.72px while every other h3 on the page
   computed to 34.8px - the same role at two sizes that share no step of the scale.
   They now take --fs-4, the step directly below the ledger's --fs-5. */
.card h3 { font-size: var(--fs-4); color: var(--text-strong); margin-bottom: var(--sp-2); }
.invert .card h3 { color: var(--inv-text); }

/* ---------- the phone menu ----------
   Measured reason this exists: below 64rem .nav__links is display:none, so before this a
   phone visitor had a wordmark and one button and could not reach a single section of an
   11,349px page. Ema: "a hamburger menu - more clear - easy to navigate".

   Geometry is set by where the thumb is. On a 390x844 phone held one-handed and low, the
   comfortable arc reaches everything below y=430. So the five links are pushed to the
   BOTTOM of the panel, not the top: the panel is a column with the label at the top and
   the links, the join button and the close control stacked into the lower half. */
.menubtn {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap); min-width: var(--tap);
  padding: 0 var(--sp-2);
  background: none; border: 1px solid var(--border); color: var(--text);
  font-family: var(--font-text); font-size: var(--fs-1);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  cursor: pointer;
}
.menubtn__bars { position: relative; width: 18px; height: 2px; background: currentColor; display: block; }
.menubtn__bars::before, .menubtn__bars::after {
  content: ""; position: absolute; left: 0; width: 18px; height: 2px; background: currentColor;
}
.menubtn__bars::before { top: -6px; }
.menubtn__bars::after  { top: 6px; }
.menubtn[aria-expanded="true"] .menubtn__bars { background: transparent; }
.menubtn[aria-expanded="true"] .menubtn__bars::before { top: 0; transform: rotate(45deg); }
.menubtn[aria-expanded="true"] .menubtn__bars::after  { top: 0; transform: rotate(-45deg); }

.menu {
  position: fixed; inset: 0; z-index: 60;
  background: var(--bg);
  padding: calc(env(safe-area-inset-top) + var(--sp-4)) var(--sp-3)
           calc(env(safe-area-inset-bottom) + var(--sp-4));
  overflow-y: auto;
}
.menu__inner { display: flex; flex-direction: column; min-height: 100%; gap: var(--sp-3); }
.menu__top { display: flex; align-items: center; justify-content: space-between; }
.menu__x {
  min-height: var(--tap); min-width: var(--tap);
  background: none; border: 0; color: var(--text-muted);
  font-family: var(--font-text); font-size: var(--fs-4); line-height: 1; cursor: pointer;
}
/* Sits at the optical centre of a phone screen, about 38% down, which is where the eye
   lands once the status bar and the header are discounted. */
.menu__card { margin-top: var(--sp-4); }
.menu__mark {
  font-family: var(--font-display); font-size: var(--fs-6); line-height: 0.85;
  color: var(--text-strong); letter-spacing: var(--tracking-display); margin: 0;
}
.menu__line { font-size: var(--fs-2); color: var(--text); margin: var(--sp-2) 0 0; }
.menu__line--dim { color: var(--text-muted); }
.menu__meta {
  font-size: var(--fs-1); color: var(--text-muted);
  letter-spacing: var(--tracking-label); text-transform: uppercase; margin: 0;
}
/* The links belong in the thumb arc rather than under the notch, and margin-top:auto is
   what used to put them there - by collecting every spare pixel into ONE gap above the
   nav. Measured at 375x812: 100px of nothing between the lede and the first link, over
   the 96px a single dead run is allowed. So the slack now goes INTO the rows instead of
   above them: the nav takes the free space and the five links share it, which keeps the
   list bottom-anchored, removes the run, and makes every target bigger rather than
   leaving the same pixels empty. The cap stops a very tall screen turning five links
   into five billboards; whatever is left over after the cap collects above the nav
   again, where flex-end puts it, and on the largest phone that remainder is still small. */
.menu__nav { margin-top: var(--sp-4); flex: 1 1 auto; display: flex; flex-direction: column; justify-content: flex-end; }
.menu__nav a:not(.btn) {
  font-family: var(--font-display); font-size: var(--fs-5); line-height: 1.1;
  color: var(--text-strong); text-decoration: none;
  letter-spacing: var(--tracking-display);
  padding-block: var(--sp-2);
  border-bottom: 1px solid var(--border);
  min-height: 48px; max-height: 5rem; flex: 1 1 auto; display: flex; align-items: center;
}
.menu__nav a:not(.btn)[aria-current="true"] { color: var(--accent); }
.menu__join { width: 100%; justify-content: center; }
.menu__close {
  min-height: var(--tap); width: 100%;
  background: none; border: 1px solid var(--border); color: var(--text-muted);
  font-family: var(--font-text); font-size: var(--fs-1);
  letter-spacing: var(--tracking-label); text-transform: uppercase; cursor: pointer;
}
body.menu-open { overflow: hidden; }

/* THE MENU ON A SIDEWAYS PHONE. Measured at 844x390 before this: the panel's content ran
   914px inside a 390px viewport and the FIRST link sat 4px from the bottom edge, so every
   navigation link was below the fold and a menu that exists to save scrolling could only
   be used by scrolling. The display wordmark alone is most of that height.
   So on a short viewport the panel compacts rather than scrolls: the mark drops to a
   heading size, the second line of the standing block yields (the FACT - Vol. 02, Vienna,
   October - stays; the sentence restating free entry goes, because it is on the page
   behind), and the rows fall back to the 44px floor. 30rem is 480px, the same threshold
   nav.js uses to stand the ask bar down, and `askbar-threshold-agrees` keeps them equal. */
@media (max-height: 30rem) {
  .menu { padding-block: calc(env(safe-area-inset-top) + var(--sp-2))
                         calc(env(safe-area-inset-bottom) + var(--sp-2)); }
  .menu__inner { gap: var(--sp-2); }
  .menu__card { margin-top: var(--sp-2); }
  .menu__mark { font-size: var(--fs-4); line-height: 1; }
  .menu__line--dim { display: none; }
  /* A landscape phone is short and WIDE, and a single column of links wastes the one
     dimension it has to spare. Two columns turn five rows into three and buy back 88px,
     which is what puts the JOIN button and the close control back above the fold too:
     measured 471px of panel in a 390px viewport before, 383px after. */
  .menu__nav { margin-top: var(--sp-2); display: grid; grid-template-columns: 1fr 1fr;
               column-gap: var(--sp-3); }
  .menu__nav a:not(.btn) { min-height: 44px; max-height: 44px; font-size: var(--fs-3); padding-block: 0; }
}

/* ---------- the standing ask ----------
   One tap to the sign-up card, in the window between the hero and the card. When it may
   appear is decided in nav.js and documented there; this is only how it looks.

   The two measurements that shaped it:
   - the button sits at least 8px clear of the bottom edge, plus the home indicator, so it
     is never inside the strip where iOS and Android claim the swipe;
   - it slides rather than appears, because a 61px block materialising under a thumb that
     is mid-scroll reads as a misfire. 180ms is the site's one duration. */
.askbar {
  position: fixed; left: 0; right: 0; max-width: 100vw; bottom: 0; z-index: 40;
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3)
           calc(env(safe-area-inset-bottom) + var(--sp-2) + 8px);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(10px);
  border-top: 1px solid var(--border);
  transform: translateY(0);
  visibility: visible;
  transition: transform var(--dur) var(--ease), visibility 0s;
}
/* [hidden] is display:none, which cannot be transitioned, so the element stays laid out
   and is moved out of the way instead. visibility:hidden is what does the real hiding:
   without it the JOIN link inside an off-screen bar stays in the tab order and a keyboard
   user tabs into a button they cannot see. It is delayed by one duration so the slide
   still plays, and it also takes the bar out of the accessibility tree, which the plain
   [hidden] attribute would have done on its own. */
.askbar[hidden] {
  display: flex !important;
  transform: translateY(110%);
  visibility: hidden;
  pointer-events: none;
  transition: transform var(--dur) var(--ease), visibility 0s linear var(--dur);
}
.askbar .btn { flex: 0 0 auto; min-height: 3rem; min-width: 0; }   /* 48px, not sitting on the 44 floor */

/* The bar is fixed, so without this it covers whatever is at the bottom of the screen.
   Measured before: sampling every 200px from y=1000 to y=3200 at 390x844, it overlapped
   visible text at 8 of 12 positions, worst 53px of a 69px bar over "You do not have to
   know anyone". The reservation is CONSTANT, not toggled with the bar: a padding that
   came and went would move the page under a thumb every time the bar slid, which is a
   worse fault than the one it fixes. --askbar-h is published by nav.js from the bar's own
   measured height, so it cannot drift from the thing it is reserving space for, and the
   fallback covers the no-JavaScript case where the bar never appears at all. */
html.js-askbar body { padding-bottom: var(--askbar-h, 4.3125rem); }
.askbar__note {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--fs-1); color: var(--text-muted);
  letter-spacing: var(--tracking-label); text-transform: uppercase;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* At 320 the button needs every pixel it can get, so the note yields first - and the
   button then TAKES the room, rather than leaving it empty. Measured before: at 320 the
   note computed to display:none and the button stayed 165px wide, leaving 139px of the
   usable 288px carrying nothing, 43% of the bar. */
@media (max-width: 22.5rem) {
  .askbar__note { display: none; }
  .askbar .btn { flex: 1 1 auto; }
}
@media (prefers-reduced-motion: reduce) {
  .askbar { transition: none; }
  .askbar[hidden] { display: none !important; }
}

/* Both of these are phone furniture. Above 64rem the link row is visible in the header at
   all times, so a second button and a hamburger would be two copies of the same control. */
@media (min-width: 64rem) {
  .menubtn, .menu, .askbar { display: none !important; }
}
/* The header link is hidden below the laptop breakpoint because the ask bar carries the
   same ask below the hero - but ONLY where the bar can actually appear. nav.js refuses to
   show the bar on a viewport under 480px tall, and with both rules unconditional a phone
   held sideways had no ask at all: measured at 844x390, scrolled to y=1600 of a 7183px
   document, askbar.hidden was true and .nav__join was display:none, leaving the WhatsApp
   community link as the only thing on screen resembling an invitation. The 30rem here and
   the 480 in nav.js are the same threshold and must stay that way; `askbar-threshold-agrees`
   fails the build if they drift. */
@media (max-width: 63.999rem) and (min-height: 30rem) {
  .nav__join { display: none; }        /* the ask bar carries it below the hero instead */
}
/* Below 30rem tall, .nav__join reappears (the ask bar cannot stand there) alongside the
   menu button, and .nav's own width is only as wide as those two children - so the two
   sat 16px apart, `gap`'s value, at the SAME right end of the bar. Measured at 844x390:
   Menu and Join WHYZ 16px apart, needs 200px (mobile100 item 6). Below this height, let
   .nav claim the bar's remaining width so its two visible controls can sit at opposite
   ends of it, same as the wordmark and the bar already do. */
@media (max-width: 63.999rem) and (max-height: 29.999rem) {
  .nav { flex: 1; justify-content: space-between; }
}

/* ---------- folding sections (phone only) ----------
   Measured reason: the home page was 12,212px at 390x844, which is 14.5 screens, with
   every section open at full length and nothing skippable. Ema: "on mobile it always
   looks more overwhelming ... overwhelming blocks of information".

   The fold is applied by script, at phone widths only, and only to elements AFTER the
   heading. Three consequences, all deliberate:
     - with JavaScript off, nothing folds and the page is exactly what it was;
     - above 64rem nothing folds, so the laptop version Ema prefers is untouched;
     - the meta label and the h2 stay in the document at all times, so the outline a
       screen reader hears is the same open or closed.
   Nothing here uses <details>: the sections are grid children of .edge, and wrapping
   them in a <details> would take them out of that grid at every width. */
.foldbtn {
  display: none;                                  /* only the script turns this on */
  width: 100%; margin-top: var(--sp-3);
  min-height: var(--tap);
  align-items: center; justify-content: space-between; gap: var(--sp-3);
  padding: var(--sp-2) 0;
  background: none; border: 0;
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
  color: var(--text-muted); text-align: left; cursor: pointer;
  font-family: var(--font-text); font-size: var(--fs-3); line-height: var(--lh-body);
}
.invert .foldbtn { color: var(--inv-muted); border-color: var(--inv-border); }
/* min-width: 0 is load-bearing, same reason as .footer__grid > * above: a flex item
   defaults to min-width:auto, so the longest data-fold label ("Anyone can come. The board
   at the door does the introducing.") refused to wrap and pushed .foldbtn__sign 35px past
   the viewport at 200% zoom (mobile100#96). */
.foldbtn__text { min-width: 0; }
.foldbtn__sign {
  flex: 0 0 auto; width: 1.5rem; text-align: center;
  font-size: var(--fs-4); line-height: 1; color: var(--accent);
}
.invert .foldbtn__sign { color: var(--inv-accent); }
@media (max-width: 63.999rem) {
  .js-fold .foldbtn { display: flex; }
  .js-fold [data-fold]:not(.is-open) .fold-hide { display: none; }
  /* A folded section is a row, not a chapter, so it does not get chapter-sized padding. */
  /* A COLLAPSED section is a list row, not a chapter, and it kept a chapter's breathing
     room. Measured at 390x844 with everything folded, content-to-chrome per screen ran
     87, 49, 51, 77, 68, 76, 54, 57 - and screens two and three are exactly the run of
     folded sections, each one 32px of padding around a heading and a single line. */
  .js-fold [data-fold]:not(.is-open) { padding-block: var(--sp-3); }
}

/* ---------- the questions list ----------
   Measured before this existed: each answer ran 1368px and 140 characters at 1440, which
   is 87% past the upper bound of the reading band while the rest of the page sat at 62.
   A definition list has no intrinsic measure, so it takes the same one everything else
   on this page takes. */
.qa { margin: 0; }
.qa dt {
  font-family: var(--font-display); font-size: var(--fs-4); line-height: 1.1;
  color: var(--text-strong); letter-spacing: var(--tracking-display);
  margin-top: var(--sp-4); padding-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.qa dt:first-child { margin-top: 0; }
.qa dd { margin: var(--sp-2) 0 0; max-width: 62ch; color: var(--text); }
.invert .qa dt { color: var(--inv-text); border-color: var(--inv-border); }

/* ---------- grounds: a photograph used as a surface, not as a picture ----------
   Ema: "you can do little cut outs or show half a picture or just put it somewhere
   darkened in the background". A ground is the third of those. The rule it obeys: a
   ground may never cost contrast. Everything over it is measured against the darkened
   result, not against the photograph.

   Two of them exist, and both are earning something specific rather than decorating:
   - behind the sign-up card sits the actual collaboration board from the door of Vol. 01,
     because the form is a copy of a card from that board and the claim should be visible;
   - behind the phone menu sits the room, so opening the menu feels like the night rather
     than like a settings screen. It costs nothing until opened: the panel is `hidden`, so
     the browser never fetches the image until the panel is shown. */
.ground { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
.ground img { width: 100%; height: 100%; object-fit: cover; }
.ground::after {
  content: ""; position: absolute; inset: 0;
  background: color-mix(in srgb, var(--bg) 86%, transparent);
}
/* The frame is the only place the board shows: a margin of photograph around an opaque
   card. No text sits on it, so the veil can stay light enough for the board to be legible
   as a board. */
.cardframe { position: relative; padding: var(--sp-4); margin-top: var(--sp-4); }
@media (min-width: 48rem) { .cardframe { padding: var(--sp-5) var(--sp-4); } }
.cardframe > .cardform { position: relative; z-index: 1; }
.ground--menu img { object-position: 50% 40%; }
.ground--menu::after { background: color-mix(in srgb, var(--bg) 90%, transparent); }
.menu__inner { position: relative; z-index: 1; }

/* ---------- form status: never colour alone ----------
   Six selectors distinguished success from failure only by changing `color`. For anyone
   who cannot separate the two hues that is one message in one colour, which is the whole
   of WCAG 1.4.1. The mark carries the meaning now, and the colour reinforces it. */
.form__status[data-state]::before {
  display: inline-block; width: 1.25em; font-weight: 700;
}
.form__status[data-state="ok"]::before    { content: "\2713 "; }   /* check */
.form__status[data-state="error"]::before { content: "\26A0 "; }   /* warning */
.form__status[data-state="ok"],
.form__status[data-state="error"] {
  border-left: 2px solid currentColor; padding-left: var(--sp-2);
}
