@charset "UTF-8";
/* =====================================================================
   E-Venz Africa Event Hub — design system
   Ported from the Tailwind theme (tailwind.config.ts + index.css):
   brand #c92737 / #1f1629, Lato, --radius 0.75rem, light + dark themes.
   Self-contained: no CDN, no build step.
   ===================================================================== */

:root {
  --primary: #c92737;
  --primary-600: #b3202f;
  --primary-50: #fef2f2;
  --primary-100: #fee2e2;
  --secondary: #1f1629;
  --secondary-800: #2c2038;

  --bg: #ffffff;
  --bg-alt: #f8f7fb;
  --fg: #101418;
  --muted: #e5e3ea;
  --muted-fg: #6b6880;
  --card: #ffffff;
  --card-fg: #101418;
  --border: #e6eaef;
  --input-bg: #f7f9fa;
  --ring: rgba(201, 39, 55, .35);
  --destructive: #ef2b41;
  --success: #12a05e;
  --warning: #d08700;

  /* Status colours split by the job they do, because one value cannot do
     both. A fill carries white text on it; a foreground sits on a card.
     --destructive at #ef2b41 gives white text only 4.12:1, so the fill
     is a shade deeper, and the foregrounds are set per theme below —
     no red is legible on both a white card and a near-black one.
     Every value here was measured, not chosen by eye. */
  --danger: #e11d33;          /* fill: 4.75 with white text            */
  --danger-fg: #b3151f;       /* text: 6.90 on a card, 5.69 on a tint  */
  --warning-fg: #8a5a00;      /* text: 5.93 on a card, 5.15 on a tint  */
  --ok: #1c7c43;              /* text: 5.23 on a card, 4.53 on a tint  */

  /* Secondary text has two jobs and one of them is harder: --muted-fg
     reads on a card, but a badge sits on --muted, and the same grey
     against that only reaches 4.21. */
  --muted-strong: #5b586e;    /* 5.38 on --muted                       */

  /* Fills for a whole panel of white text — the scanner's result banner
     is the case that matters, read on a phone at a door in daylight.
     --success and --warning are tuned as accents on a card and give
     white only 3.38 and 2.94, so a banner painted in them puts its
     small print below AA. These are the same hues carried deeper. */
  --success-fill: #0f7a48;    /* 5.39 with white text                  */
  --warning-fill: #b45309;    /* 5.02                                  */

  /* The brand as text. #c92737 reads on white at 5.48 but only 3.28 on
     a dark card, and 4.49 on the brand's own tint — where a badge puts
     it. So the word is carried a step deeper than the fill. */
  --primary-fg: #b3202f;      /* 6.64 on a card, 5.43 on a tint        */

  /* --success is a fill colour; as text on white it is only 3.38, which
     is why --ok exists rather than reusing it. */

  --accent: var(--primary);   /* one accent, and it is the brand        */
  --primary-rgb: 201, 39, 55; /* commas: rgba(var(--primary-rgb), .06)  */

  --radius: 0.75rem;
  --radius-sm: 0.5rem;
  --radius-lg: 1rem;

  --shadow-sm: 0 1px 2px rgba(16, 20, 24, .06);
  --shadow: 0 2px 8px rgba(16, 20, 24, .08);
  --shadow-lg: 0 12px 32px rgba(16, 20, 24, .12);

  /* ---- the type scale --------------------------------------------
     The interface range had grown 22 sizes between .58rem and 1rem —
     .84, .85, .86 and .87 all present, which nobody chose on purpose.
     Six steps now, each one a value already common in the file, so the
     largest move was .03rem: half a pixel, invisible on its own and
     the reason nothing lines up when it is repeated 150 times.

     Above 1rem is display type — hero headings, the scanner's verdict,
     a certificate's name — chosen per use and left alone. Snapping
     those would be a redesign, not a tidy-up. */
  --t-2xs:  0.68rem;   /* micro caps: chips, badges          */
  --t-xs:   0.74rem;   /* meta, timestamps                   */
  --t-sm:   0.8rem;    /* secondary lines                    */
  --t-md:   0.86rem;   /* dense table and card text          */
  --t-base: 0.93rem;   /* controls and body                  */
  --t-lg:   1rem;      /* the top of the interface range     */

  --font-sans: 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* Glass: one set of tokens so every translucent surface agrees. Tuned
     separately per theme — the same alpha that reads as frosted on a
     light background reads as dirty on a dark one. */
  --glass-bg: rgba(255, 255, 255, 0.72);
  --glass-line: rgba(255, 255, 255, 0.55);
  --glass-shadow: 0 8px 32px rgba(16, 24, 40, 0.10);
  --glass-blur: 18px;

  --container: 1200px;
  --header-h: 68px;
}

html[data-theme="dark"] {
  --glass-bg: rgba(22, 20, 30, 0.62);
  --glass-line: rgba(255, 255, 255, 0.10);
  --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.42);

  /* The fills keep their value — white text still reads on them — but a
     status colour used as TEXT has to lighten against a #16161d card.
     The light-theme reds and ambers land at 2.3–3.0 here, which is why
     these are redefined rather than inherited. Measured on the card. */
  --danger-fg: #f4566a;       /* 5.47 on the card, 4.92 on a tint       */
  --warning-fg: #e0a92a;      /* 8.47 / 6.97                            */
  --ok: #34d399;              /* 9.36 / 8.02                            */
  --primary-fg: #e8636f;      /* 5.52 / 4.85 — the brand lifted to read */
  --muted-strong: #9b98ab;    /* 5.89 on --muted; the card value holds  */

  --bg: #0b0b0e;
  --bg-alt: #131319;
  --fg: #e8e9ea;
  --muted: #1e1e26;
  --muted-fg: #9b98ab;
  --card: #16161d;
  --card-fg: #e8e9ea;
  --border: #26262f;
  --input-bg: #1b1b23;
  --primary-50: #2a1418;
  --primary-100: #3a181e;
  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }

html { font-size: 16px; -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.65;
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }
/* Links wear the brand as a word, not as a fill: --primary reaches
   only 3.28 against a dark card, so every body link on the site was
   under AA in dark mode. */
a { color: var(--primary-fg); text-decoration: none; }
a:hover { text-decoration: underline; }

h1, h2, h3, h4, h5 { margin: 0 0 .5em; line-height: 1.25; font-weight: 700; letter-spacing: -.01em; }
h1 { font-size: 2.25rem; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.3rem; }
h4 { font-size: 1.1rem; }
p  { margin: 0 0 1em; }

hr { border: 0; border-top: 1px solid var(--border); margin: 2rem 0; }

/* ---------------------------------------------------------------- layout */
.container { max-width: var(--container); margin: 0 auto; padding: 0 20px; }
.container-narrow { max-width: 780px; margin: 0 auto; padding: 0 20px; }
.section { padding: 56px 0; }
.section-sm { padding: 32px 0; }
.section-alt { background: var(--bg-alt); }

.grid { display: grid; gap: 24px; }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-auto { grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); }

.flex { display: flex; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: 6px; } .gap-2 { gap: 12px; } .gap-3 { gap: 18px; }
.wrap { flex-wrap: wrap; }

/* On a phone a row of controls either wraps or shoves the page
   sideways, and sideways is the worse answer — the skill's own rule is
   that page-level horizontal scroll is a blocker. 92 of the 127 flex
   rows in the codebase already ask for wrap; the other 35 are rows
   nobody happened to open at 375px. A button row on Ticketing was one
   of them, reaching 421px inside a 360px viewport. */
@media (max-width: 640px) { .flex { flex-wrap: wrap; } }




.grow { flex: 1 1 auto; }

.text-center { text-align: center; }
.text-right { text-align: right; }
.muted { color: var(--muted-fg); }
.small { font-size: 0.86rem; }
.tiny { font-size: 0.8rem; }
.strong { font-weight: 700; }
.nowrap { white-space: nowrap; }
.mt-0 { margin-top: 0; } .mt-1 { margin-top: 8px; } .mt-2 { margin-top: 16px; } .mt-3 { margin-top: 24px; } .mt-4 { margin-top: 36px; }
.mb-0 { margin-bottom: 0; } .mb-1 { margin-bottom: 8px; } .mb-2 { margin-bottom: 16px; } .mb-3 { margin-bottom: 24px; } .mb-4 { margin-bottom: 36px; }

.section-head { margin-bottom: 32px; }
.section-head h2 { margin-bottom: 6px; }
.section-head p { color: var(--muted-fg); margin: 0; }

/* ---------------------------------------------------------------- header */
.site-header {
  position: sticky; top: 0; z-index: 50;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--border);
}
.site-header .inner { display: flex; align-items: center; gap: 24px; height: var(--header-h); }

.brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 1.1rem; color: var(--fg); text-decoration: none; letter-spacing: -.02em; }
.brand:hover { text-decoration: none; opacity: .85; }
.brand-mark {
  width: 32px; height: 32px; border-radius: 9px; flex: 0 0 auto;
  background: linear-gradient(135deg, var(--primary), var(--secondary));
  color: #fff; display: grid; place-items: center; font-size: 0.8rem; font-weight: 800;
}
.brand em { font-style: normal; color: var(--primary); }

/* An uploaded logo stands in for the wordmark. Height-capped so a
   large upload cannot blow out the header. */
.brand-logo { max-height: 40px; width: auto; max-width: 220px; object-fit: contain; }

.nav { display: flex; align-items: center; gap: 22px; margin-left: 8px; }
.nav a { color: var(--muted-fg); font-weight: 600; font-size: 0.93rem; text-decoration: none; padding: 6px 0; border-bottom: 2px solid transparent; }
.nav a:hover { color: var(--fg); text-decoration: none; }
.nav a.active { color: var(--primary-fg); border-bottom-color: var(--primary); }

.header-actions { margin-left: auto; display: flex; align-items: center; gap: 10px; }

.nav-toggle, .theme-toggle {
  display: none; background: none; border: 1px solid var(--border);
  border-radius: var(--radius-sm); width: 38px; height: 38px;
  cursor: pointer; color: var(--fg); font-size: 1rem; line-height: 1;
}
.theme-toggle { display: grid; place-items: center; }
.theme-toggle:hover { background: var(--muted); }

@media (max-width: 900px) {
  .nav-toggle { display: grid; place-items: center; }
  .nav {
    display: none; position: absolute; top: var(--header-h); left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--bg); border-bottom: 1px solid var(--border);
    padding: 8px 20px 16px; box-shadow: var(--shadow);
  }
  .nav.open { display: flex; }
  .nav a { padding: 11px 0; border-bottom: 1px solid var(--border); }
  .nav a.active { border-bottom-color: var(--border); }
}

/* user menu */
.usermenu { position: relative; }
.usermenu-btn {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  background: var(--muted); border: 1px solid var(--border);
  border-radius: 999px; padding: 5px 12px 5px 5px; font: inherit; font-size: 0.86rem; font-weight: 600; color: var(--fg);
}
.usermenu-btn:hover { border-color: var(--primary); }
.avatar {
  width: 28px; height: 28px; border-radius: 50%; flex: 0 0 auto;
  background: var(--primary); color: #fff; display: grid; place-items: center;
  font-size: 0.8rem; font-weight: 700; text-transform: uppercase;
}
.usermenu-panel {
  display: none; position: absolute; right: 0; top: calc(100% + 8px); min-width: 220px;
  background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 6px; z-index: 60;
}
.usermenu-panel.open { display: block; }
.usermenu-panel a, .usermenu-panel button {
  display: block; width: 100%; text-align: left; padding: 9px 12px; border-radius: var(--radius-sm);
  color: var(--fg); font-size: 0.93rem; font-weight: 500; background: none; border: 0; cursor: pointer; font-family: inherit;
}
.usermenu-panel a:hover, .usermenu-panel button:hover { background: var(--muted); text-decoration: none; }
.usermenu-head { padding: 10px 12px 8px; border-bottom: 1px solid var(--border); margin-bottom: 6px; }
.usermenu-head strong { display: block; font-size: 0.93rem; }
.usermenu-head span { font-size: 0.8rem; color: var(--muted-fg); }
.usermenu-sep { height: 1px; background: var(--border); margin: 6px 0; }

/* ---------------------------------------------------------------- buttons */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 20px; border-radius: var(--radius-sm);
  font-family: inherit; font-size: 0.93rem; font-weight: 600; line-height: 1.2;
  border: 1px solid transparent; cursor: pointer; text-decoration: none;
  transition: filter .15s, background .15s, border-color .15s;
  white-space: nowrap;
}
.btn:hover { text-decoration: none; }
.btn:focus-visible { outline: 3px solid var(--ring); outline-offset: 1px; }
.btn:disabled, .btn.disabled { opacity: .55; pointer-events: none; }

.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-600); color: #fff; }
.btn-secondary { background: var(--secondary); color: #fff; }
.btn-secondary:hover { background: var(--secondary-800); color: #fff; }
.btn-outline { background: transparent; color: var(--fg); border-color: var(--border); }
.btn-outline:hover { border-color: var(--primary); color: var(--primary); }
.btn-ghost { background: transparent; color: var(--muted-fg); }
.btn-ghost:hover { background: var(--muted); color: var(--fg); }
.btn-danger { background: var(--destructive); color: #fff; }
.btn-danger:hover { filter: brightness(.92); color: #fff; }
.btn-sm { padding: 6px 13px; font-size: 0.86rem; }
.btn-lg { padding: 13px 28px; font-size: 1rem; }
.btn-block { width: 100%; }

/* ---------------------------------------------------------------- cards */
.card {
  background: var(--card); color: var(--card-fg);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); overflow: hidden;
}
.card-body { padding: 22px; }
.card-head { padding: 16px 22px; border-bottom: 1px solid var(--border); font-weight: 700; }
.card-foot { padding: 14px 22px; border-top: 1px solid var(--border); background: var(--bg-alt); }

/* event card */
.event-card { display: flex; flex-direction: column; height: 100%; transition: transform .18s, box-shadow .18s; }
.event-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lg); }
.event-card .thumb {
  position: relative; aspect-ratio: 16 / 9; background: var(--muted);
  display: grid; place-items: center; overflow: hidden;
}
.event-card .thumb img { width: 100%; height: 100%; object-fit: cover; }
.event-card .thumb .placeholder {
  width: 100%; height: 100%; display: grid; place-items: center;
  background: linear-gradient(135deg, var(--secondary), var(--primary));
  color: #fff; font-size: 1.6rem; font-weight: 800; letter-spacing: .04em; opacity: .92;
}
.event-card .body { padding: 18px; display: flex; flex-direction: column; gap: 8px; flex: 1 1 auto; }
.event-card h3 { font-size: 1.05rem; margin: 0; }
.event-card h3 a { color: var(--fg); }
.event-card h3 a:hover { color: var(--primary); text-decoration: none; }
.event-card .meta { display: flex; flex-direction: column; gap: 4px; font-size: 0.86rem; color: var(--muted-fg); }
.event-card .meta span { display: flex; align-items: center; gap: 7px; }
.event-card .foot { margin-top: auto; padding-top: 12px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }

.badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px; border-radius: 999px;
  font-size: 0.74rem; font-weight: 700; letter-spacing: .02em;
  background: var(--muted); color: var(--muted-strong); text-transform: uppercase;
}
/* A badge is a tint with a word on it, which is the hardest place to
   put a status colour: the accent that reads on a white card is sitting
   on its own pale self here. Measured on the composited tint, the
   accents gave 2.55 (warning), 2.92 (success), 3.40 (danger), 4.49
   (primary) and 4.21 (neutral) — the whole family under AA, on the
   component that appears on every event card. These are the same hues
   at the weight the ground needs. */
.badge-primary   { background: var(--primary-100); color: var(--primary-fg); }
.badge-featured  { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; }
.badge-success   { background: rgba(18,160,94,.13); color: var(--ok); }
.badge-warning   { background: rgba(208,135,0,.14); color: var(--warning-fg); }
.badge-danger    { background: rgba(239,43,65,.13); color: var(--danger-fg); }
.badge-free      { background: rgba(18,160,94,.13); color: var(--ok); }

.badge-float { position: absolute; top: 12px; left: 12px; z-index: 2; }
.badge-float-right { position: absolute; top: 12px; right: 12px; z-index: 2; left: auto; }

/* ------------------------------------------------------- event content
   Videos keep a 16:9 box whatever the provider does inside the iframe;
   sponsor logos are given equal room rather than equal width, because a
   wordmark and a roundel at the same width look nothing alike. */
.ev-videos { display: grid; gap: 18px; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.ev-video { margin: 0; }
.ev-video iframe { width: 100%; aspect-ratio: 16 / 9; border: 0; border-radius: 10px;
                   background: #000; display: block; }
.ev-video figcaption { font-size: 0.86rem; margin-top: 6px; }
.ev-video figcaption span { display: block; color: var(--muted-fg); font-size: 0.8rem; }

.ev-sponsors { display: flex; flex-wrap: wrap; gap: 18px; align-items: center; }
.ev-sponsor { display: grid; place-items: center; min-width: 120px; height: 56px;
              padding: 6px 12px; border: 1px solid var(--border); border-radius: 10px; }
.ev-sponsor img { max-height: 40px; max-width: 150px; object-fit: contain; }
.ev-sponsor span { font-size: 0.86rem; font-weight: 600; }

.ev-links { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.ev-links li a { font-weight: 600; }
.ev-links li span { display: block; font-size: 0.8rem; color: var(--muted-fg); }

/* ------------------------------------------------ what a ticket covers
   A priced tier is a promise about access, and the promise has to be
   readable at the moment of choosing. A tick per line rather than a
   paragraph: the eye compares two lists, it does not compare two
   sentences. */
.ticket-includes {
  list-style: none; margin: 8px 0 0; padding: 0;
  font-size: 0.8rem; color: var(--muted-fg); display: grid; gap: 3px;
}
.ticket-includes li { position: relative; padding-left: 18px; line-height: 1.35; }
.ticket-includes li::before {
  content: "\2713"; position: absolute; left: 0; top: 0;
  color: var(--success); font-weight: 700;
}
.ticket-includes li.count {
  padding-left: 18px; opacity: .75; font-variant-numeric: tabular-nums;
}
.ticket-includes li.count::before { content: "\00B7"; font-size: 1.2em; line-height: 1; }

/* ---------------------------------------------------------------- hero */
.hero {
  position: relative; color: #fff; overflow: hidden;
  background: linear-gradient(135deg, var(--secondary) 0%, #3a1f36 55%, var(--primary) 130%);
}
.hero .container { position: relative; z-index: 2; padding-top: 76px; padding-bottom: 76px; }
.hero h1 { font-size: clamp(2rem, 5vw, 3.1rem); margin-bottom: 14px; color: #fff; }
.hero p.lead { font-size: 1.1rem; max-width: 620px; color: rgba(255,255,255,.86); margin-bottom: 26px; }
.hero .actions { display: flex; gap: 12px; flex-wrap: wrap; }
.hero .btn-outline { color: #fff; border-color: rgba(255,255,255,.5); }
.hero .btn-outline:hover { background: rgba(255,255,255,.12); color: #fff; border-color: #fff; }
.hero-bg {
  position: absolute; inset: 0; z-index: 1; opacity: .22;
  background-size: cover; background-position: center;
}
.hero-stats { display: flex; gap: 38px; flex-wrap: wrap; margin-top: 38px; }
.hero-stats div strong { display: block; font-size: 1.9rem; line-height: 1.1; }
.hero-stats div span { font-size: 0.86rem; color: rgba(255,255,255,.72); }

/* ---- home hero: copy beside a stack of real generated flyers ---------- */
.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
  gap: 48px;
  align-items: center;
}
.hero-copy { min-width: 0; }

.hero-eyebrow {
  text-transform: uppercase;
  letter-spacing: .08em;
  font-size: 0.74rem;
  font-weight: 700;
  color: rgba(255,255,255,.62);
  margin-bottom: 14px;
}

.hero-search {
  display: flex;
  gap: 8px;
  max-width: 520px;
  margin-bottom: 18px;
}
.hero-search input {
  flex: 1 1 auto;
  min-width: 0;
  background: rgba(255,255,255,.12);
  border-color: rgba(255,255,255,.28);
  color: #fff;
}
.hero-search input::placeholder { color: rgba(255,255,255,.6); opacity: 1; }
.hero-search input:focus {
  background: rgba(255,255,255,.18);
  border-color: rgba(255,255,255,.7);
  box-shadow: 0 0 0 3px rgba(255,255,255,.16);
  color: #fff;
}
.hero-search .btn { flex: 0 0 auto; }

/* A text-weight action that still reads as a link on the dark hero. */
.btn-ghost-light {
  background: transparent;
  border: 1px solid transparent;
  color: rgba(255,255,255,.9);
}
.btn-ghost-light:hover { color: #fff; background: rgba(255,255,255,.1); }

.hero-art { position: relative; }

/* Three flyers fanned out.

   This once declared aspect-ratio: 1/1 on the assumption that the source
   images were square. Real flyers are portrait — the ones on this
   platform are 650x900 — so a card rendered 30px taller than its
   container and sat on top of the caption beneath it.

   The fix is to stop assuming: the first card sits in normal flow and
   gives the stack its height, whatever shape the artwork is, and the
   others overlay it. The padding is the room the fan's lower cards need
   for their offsets. */
.flyer-stack {
  position: relative;
  max-width: 400px;
  margin: 0 auto;
  padding-bottom: 8%;
}

/* The room the fan needs below depends on how far its lowest card is
   offset, which depends on how many there are. */
.flyer-stack.stack-2 { padding-bottom: 13%; }
.flyer-stack.stack-3 { padding-bottom: 19%; }
.flyer-card {
  position: absolute;
  width: 74%;
  border-radius: var(--radius);
  box-shadow: 0 18px 44px rgba(0,0,0,.45);
  border: 1px solid rgba(255,255,255,.14);
  transition: transform .25s ease;
}
.flyer-card-1 { position: relative; top: 6%; left: 0; transform: rotate(-7deg); z-index: 1; }
.flyer-card-2 { top: 0;   left: 13%;  transform: rotate(-1deg); z-index: 3; }
.flyer-card-3 { top: 12%; left: 26%;  transform: rotate(6deg);  z-index: 2; }

.flyer-stack:hover .flyer-card-1 { transform: rotate(-11deg) translateX(-7px); }
.flyer-stack:hover .flyer-card-3 { transform: rotate(10deg) translateX(7px); }

/* Fewer than three published designs: centre what there is rather than
   leaving the fan leaning into empty space. */
.stack-1 .flyer-card-1 {
  top: 4%; left: 13%; width: 74%; transform: rotate(-2deg);
}
.stack-1:hover .flyer-card-1 { transform: rotate(-4deg); }

.stack-2 .flyer-card-1 { top: 8%; left: 2%;  transform: rotate(-6deg); }
.stack-2 .flyer-card-2 { top: 2%; left: 22%; transform: rotate(3deg); }
.stack-2:hover .flyer-card-1 { transform: rotate(-9deg) translateX(-6px); }
.stack-2:hover .flyer-card-2 { transform: rotate(6deg) translateX(6px); }

/* The showcase row is a 3-up grid; fewer images should not stretch. */
.showcase-art:has(img:only-child) { grid-template-columns: minmax(0, 320px); justify-content: center; }
.showcase-art:has(img:nth-child(2):last-child) { grid-template-columns: repeat(2, minmax(0, 260px)); justify-content: center; }

.hero-art-note {
  text-align: center;
  font-size: 0.8rem;
  color: rgba(255,255,255,.62);
  margin: 18px 0 0;
}

@media (max-width: 900px) {
  .hero-grid { grid-template-columns: 1fr; gap: 34px; }
  .hero-art { order: -1; }
  .flyer-stack { max-width: 300px; }
  .hero-art-note { display: none; }
}

/* ---- value proposition band ------------------------------------------ */
.value-band { padding-top: 46px; padding-bottom: 46px; }
.value h3 { font-size: 1.06rem; margin-bottom: 6px; }
.value-icon {
  font-size: 1.5rem;
  width: 46px; height: 46px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius);
  background: var(--primary-50);
  margin-bottom: 12px;
}

/* ---- flyer showcase --------------------------------------------------- */
.showcase { background: var(--secondary); color: #fff; }
.showcase h2 { color: #fff; }
.showcase .muted { color: rgba(255,255,255,.72); }

.showcase-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
  gap: 52px;
  align-items: center;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: .08em;
  font-size: 0.74rem;
  font-weight: 700;
  color: var(--primary);
  margin-bottom: 10px;
}
.showcase .eyebrow { color: #ff8a95; }

.steps { list-style: none; counter-reset: step; padding: 0; margin: 26px 0; }
.steps li {
  counter-increment: step;
  position: relative;
  padding-left: 44px;
  margin-bottom: 18px;
  font-size: 0.93rem;
  color: rgba(255,255,255,.72);
}
.steps li::before {
  content: counter(step);
  position: absolute; left: 0; top: -2px;
  width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  font-size: 0.8rem;
  font-weight: 700;
}
.steps li strong { display: block; color: #fff; margin-bottom: 3px; font-size: 1rem; }

.showcase-art {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}
.showcase-art img {
  width: 100%;
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 30px rgba(0,0,0,.4);
}
/* Stagger the middle one so the row reads as a set, not a filmstrip. */
.showcase-art img:nth-child(2) { transform: translateY(-16px); }

@media (max-width: 900px) {
  .showcase-grid { grid-template-columns: 1fr; gap: 34px; }
  .showcase-art img:nth-child(2) { transform: none; }
}

/* ---- categories with counts ------------------------------------------- */
.cat-list { display: flex; flex-wrap: wrap; gap: 10px; }
.cat {
  display: inline-flex; align-items: center; gap: 10px;
  padding: 9px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  color: var(--fg);
  font-size: 0.93rem;
  font-weight: 600;
  transition: border-color .15s, background .15s, transform .15s;
}
.cat:hover {
  border-color: var(--primary);
  background: var(--primary-50);
  color: var(--primary);
  transform: translateY(-1px);
}
.cat-count {
  font-size: 0.74rem;
  font-weight: 700;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--muted);
  color: var(--muted-fg);
}
.cat:hover .cat-count { background: var(--primary); color: #fff; }

/* ---- closing call to action ------------------------------------------- */
.cta-band .actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; }
.justify-center { justify-content: center; }

/* event detail hero */
.event-hero { position: relative; background: var(--secondary); color: #fff; }
.event-hero .cover { position: absolute; inset: 0; opacity: .35; }
.event-hero .cover img { width: 100%; height: 100%; object-fit: cover; }
.event-hero .container { position: relative; z-index: 2; padding: 60px 20px; }
.event-hero h1 { color: #fff; font-size: clamp(1.7rem, 4vw, 2.6rem); }
.event-logo {
  width: 78px; height: 78px; border-radius: var(--radius); background: #fff;
  padding: 7px; box-shadow: var(--shadow); flex: 0 0 auto; margin-bottom: 16px;
}
.event-logo img { width: 100%; height: 100%; object-fit: contain; }
.event-meta-row { display: flex; flex-wrap: wrap; gap: 20px; margin-top: 16px; font-size: 0.93rem; color: rgba(255,255,255,.9); }
.event-meta-row span { display: flex; align-items: center; gap: 8px; }

/* ---------------------------------------------------------------- forms */
.field { margin-bottom: 16px; }
label { display: block; font-size: 0.86rem; font-weight: 700; margin-bottom: 6px; color: var(--fg); }
label .req { color: var(--primary); }
label .hint { font-weight: 400; color: var(--muted-fg); }

.input, input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=time], input[type=url], input[type=tel], input[type=search],
select, textarea {
  /* width:100% is not enough for a <select>: its intrinsic width comes
     from the longest option, and as a flex child min-width:auto stops
     it shrinking below that. An event picker holding "AfriLabs Annual
     Gathering 2026 -Cotonou" measured 446px inside a 360px viewport
     and pushed the whole page sideways. */
  width: 100%; max-width: 100%; min-width: 0; padding: 10px 13px;
  font-family: inherit; font-size: 0.93rem; color: var(--fg);
  background: var(--input-bg); border: 1px solid var(--border);
  border-radius: var(--radius-sm); transition: border-color .15s, box-shadow .15s;
}
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px var(--ring); background: var(--bg);
}
input::placeholder, textarea::placeholder { color: var(--muted-fg); opacity: .75; }
textarea { min-height: 120px; resize: vertical; }
select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%236b6880' d='M1 1l5 5 5-5'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 13px center; padding-right: 34px; }

.check { display: flex; align-items: flex-start; gap: 9px; font-size: 0.93rem; font-weight: 400; margin-bottom: 12px; }
.check input { width: auto; margin: 3px 0 0; flex: 0 0 auto; }
.check label { font-weight: 400; margin: 0; font-size: 0.93rem; }

.field-error { color: var(--destructive); font-size: 0.8rem; margin-top: 5px; }
.input-error { border-color: var(--destructive) !important; }

.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 640px) { .form-row { grid-template-columns: 1fr; } }

/* filter bar */
.filters { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 28px; }
.filters .field { margin: 0; min-width: 170px; }
.filters .grow { min-width: 220px; }

/* ---------------------------------------------------------------- alerts */
.alert { padding: 12px 16px; border-radius: var(--radius-sm); font-size: 0.93rem; margin-bottom: 18px; border: 1px solid; }
/* An alert is a tint with a paragraph on it, so the same rule as the
   badges: the accent that reads on a card does not read on a wash of
   itself. The warning alert measured 2.69 — the worst on any screen. */
.alert-success { background: rgba(18,160,94,.09); border-color: rgba(18,160,94,.3); color: var(--ok); }
.alert-error   { background: rgba(239,43,65,.08); border-color: rgba(239,43,65,.3); color: var(--danger-fg); }
.alert-info    { background: var(--primary-50); border-color: var(--primary-100); color: var(--primary-fg); }
.alert-warning { background: rgba(208,135,0,.09); border-color: rgba(208,135,0,.3); color: var(--warning-fg); }

/* ------------------------------------------------- form answer pills
   Scales, star ratings and NPS render as a row of selectable pills:
   the input hides, its label wears the state. */
.pillrow { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.pillrow-cap { font-size: 0.74rem; color: var(--muted-fg); padding: 0 4px; }
.pill-opt { position: relative; cursor: pointer; margin: 0; }
.pill-opt input { position: absolute; opacity: 0; pointer-events: none; }
.pill-opt span {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 38px; padding: 7px 10px; border: 1px solid var(--border);
  border-radius: 9px; font-size: 0.86rem; font-variant-numeric: tabular-nums;
  background: var(--card); transition: border-color .12s, background .12s;
}
.pill-opt:hover span { border-color: var(--fg); }
.pill-opt input:checked + span {
  border-color: var(--primary); background: var(--primary-50);
  color: var(--primary); font-weight: 700;
}
.pill-opt input:focus-visible + span { outline: 3px solid var(--ring); outline-offset: 1px; }
table.likert td, table.likert th { padding: 8px 10px; }
table.likert input[type="radio"] { width: 17px; height: 17px; }

/* ------------------------------------------------------------- toasts
   The flash channel, delivered where the eye already is. A stack in
   the corner, each with a tone icon and its own exit; the inline
   .alert stays for content that belongs in the page flow. */
.toasts {
  position: fixed; right: 18px; bottom: 18px; z-index: 130;
  display: flex; flex-direction: column; gap: 10px;
  max-width: min(430px, calc(100vw - 36px));
}
.toast {
  display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px;
  border-radius: 10px; border: 1px solid var(--border);
  background: var(--card); color: var(--card-fg);
  box-shadow: 0 12px 34px rgba(31, 22, 41, .22);
  font-size: 0.93rem; line-height: 1.45;
  animation: toast-in .3s cubic-bezier(.2, .85, .3, 1.15) both;
}
.toast-success { border-left: 3px solid var(--success); }
.toast-error   { border-left: 3px solid var(--destructive); }
.toast-info    { border-left: 3px solid var(--primary); }
.toast-ic { width: 19px; height: 19px; flex: 0 0 19px; margin-top: 1px; }
.toast-success .toast-ic { color: var(--success); }
.toast-error   .toast-ic { color: var(--destructive); }
.toast-info    .toast-ic { color: var(--primary); }
.toast-msg { flex: 1 1 auto; min-width: 0; overflow-wrap: break-word; }
.toast-x {
  background: none; border: 0; cursor: pointer; padding: 0 2px;
  color: var(--muted-fg); font-size: 1.15rem; line-height: 1;
}
.toast-x:hover { color: var(--fg); }
.toast-out { animation: toast-out .22s ease both; }
@keyframes toast-in  { from { opacity: 0; transform: translateY(16px) scale(.97); } }
@keyframes toast-out { to   { opacity: 0; transform: translateY(10px); } }
@media (max-width: 700px) {
  /* Full-width above the thumb, clear of the drawer button. */
  .toasts { left: 14px; right: 14px; bottom: 14px; max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .toast, .toast-out { animation: none; }
}

/* ---------------------------------------------------------------- misc */
.empty { text-align: center; padding: 60px 20px; color: var(--muted-fg); }
.empty .icon { font-size: 2.4rem; opacity: .35; margin-bottom: 10px; }
.empty .icon .ni { width: 44px; height: 44px; color: var(--muted-fg); }
.badge .ni { vertical-align: -2px; margin-right: 1px; }
.empty h3 { color: var(--fg); }

.pagination { display: flex; gap: 6px; justify-content: center; margin-top: 36px; flex-wrap: wrap; }
.pagination a, .pagination span {
  min-width: 38px; height: 38px; padding: 0 12px; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  color: var(--fg); font-size: 0.86rem; font-weight: 600; text-decoration: none;
}
.pagination a:hover { border-color: var(--primary); color: var(--primary); text-decoration: none; }
.pagination .current { background: var(--primary); border-color: var(--primary); color: #fff; }
.pagination .gap { border: 0; }

/* Long-form text sits in the common 1200px frame like every other
   page, but the words themselves keep a readable line: a full-width
   paragraph runs to about 150 characters, which the eye loses. */
.prose { font-size: 1rem; line-height: 1.75; max-width: 72ch; }
.prose img { border-radius: var(--radius); margin: 1.4em 0; }
.prose h2 { font-size: 1.45rem; margin-top: 1.8em; }
.prose h3 { font-size: 1.2rem; margin-top: 1.5em; }
.prose ul, .prose ol { padding-left: 1.4em; margin-bottom: 1.2em; }
.prose li { margin-bottom: .4em; }
.prose blockquote {
  margin: 1.4em 0; padding: 4px 0 4px 18px;
  border-left: 3px solid var(--primary); color: var(--muted-fg); font-style: italic;
}
.prose table { width: 100%; border-collapse: collapse; margin: 1.4em 0; }
.prose th, .prose td { border: 1px solid var(--border); padding: 9px 12px; text-align: left; }
.prose a { text-decoration: underline; }

.tag-list { display: flex; flex-wrap: wrap; gap: 7px; }
.tag { padding: 3px 10px; border-radius: 999px; background: var(--muted); color: var(--muted-strong); font-size: 0.74rem; font-weight: 600; }
a.tag:hover { background: var(--primary-100); color: var(--primary); text-decoration: none; }

.sidebar-sticky { position: sticky; top: calc(var(--header-h) + 20px); }

.detail-grid { display: grid; grid-template-columns: minmax(0,1fr) 340px; gap: 32px; align-items: start; }
/* minmax(0,1fr), not 1fr. A bare 1fr means minmax(auto,1fr), so the grid
   item keeps min-width:auto and refuses to shrink below its content —
   which is how a horizontally scrolling strip inside this grid pushed
   the whole page sideways on a phone. The desktop rule above already
   had this right; the mobile one did not. */
@media (max-width: 940px) { .detail-grid { grid-template-columns: minmax(0, 1fr); } }

.info-list { list-style: none; margin: 0; padding: 0; }
.info-list li { display: flex; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--border); font-size: 0.93rem; }
.info-list li:last-child { border-bottom: 0; }
.info-list .k { color: var(--muted-fg); flex: 0 0 92px; }
.info-list .v { font-weight: 600; word-break: break-word; }

/* ---------------------------------------------------------------- footer */
.site-footer { background: var(--secondary); color: rgba(255,255,255,.72); margin-top: 64px; }
.site-footer a { color: rgba(255,255,255,.72); text-decoration: none; }
.site-footer a:hover { color: #fff; text-decoration: none; }
.site-footer .cols { display: grid; grid-template-columns: 1.6fr 1fr 1fr 1.4fr; gap: 36px; padding: 52px 0 36px; }
.site-footer h4 { color: #fff; font-size: 0.8rem; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 14px; }
.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li { margin-bottom: 9px; font-size: 0.93rem; }
.site-footer .brand { color: #fff; margin-bottom: 12px; }
.site-footer .bottom {
  border-top: 1px solid rgba(255,255,255,.12); padding: 20px 0;
  display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; font-size: 0.86rem;
}
.social { display: flex; gap: 10px; margin-top: 14px; }
.social a {
  width: 34px; height: 34px; border-radius: 50%; background: rgba(255,255,255,.1);
  display: grid; place-items: center; font-size: 0.8rem; font-weight: 700; color: #fff;
}
.social a:hover { background: var(--primary); }

.newsletter { display: flex; gap: 8px; margin-top: 12px; }
.newsletter input { background: rgba(255,255,255,.09); border-color: rgba(255,255,255,.18); color: #fff; }
.newsletter input::placeholder { color: rgba(255,255,255,.5); }
.newsletter input:focus { background: rgba(255,255,255,.14); border-color: #fff; box-shadow: none; }

@media (max-width: 900px) { .site-footer .cols { grid-template-columns: 1fr 1fr; gap: 28px; } }
@media (max-width: 560px) { .site-footer .cols { grid-template-columns: 1fr; } }

/* ---------------------------------------------------------------- auth pages */
.auth-wrap { min-height: calc(100vh - var(--header-h)); display: grid; place-items: center; padding: 48px 20px; background: var(--bg-alt); }
.auth-card { width: 100%; max-width: 440px; }
.auth-card.wide { max-width: 620px; }
.auth-head { text-align: center; margin-bottom: 26px; }
.auth-head h1 { font-size: 1.6rem; margin-bottom: 6px; }
.auth-head p { color: var(--muted-fg); margin: 0; font-size: 0.93rem; }
.auth-foot { text-align: center; margin-top: 20px; font-size: 0.93rem; color: var(--muted-fg); }

.role-switch { display: flex; gap: 0; background: var(--muted); border-radius: var(--radius-sm); padding: 4px; margin-bottom: 20px; }
.role-switch label {
  flex: 1; text-align: center; padding: 8px; border-radius: calc(var(--radius-sm) - 2px);
  font-size: 0.86rem; font-weight: 600; cursor: pointer; margin: 0; color: var(--muted-fg);
}
.role-switch input { display: none; }
.role-switch input:checked + label { background: var(--card); color: var(--primary); box-shadow: var(--shadow-sm); }

.pw-meter { height: 4px; border-radius: 2px; background: var(--muted); margin-top: 7px; overflow: hidden; }
.pw-meter i { display: block; height: 100%; width: 0; transition: width .2s, background .2s; }

/* ---------------------------------------------------------------- pricing */
.price-card { display: flex; flex-direction: column; height: 100%; position: relative; }
.price-card.featured { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary), var(--shadow); }
.price-card .ribbon {
  position: absolute; top: 0; right: 22px; background: var(--primary); color: #fff;
  font-size: 0.68rem; font-weight: 700; padding: 4px 11px; border-radius: 0 0 6px 6px; text-transform: uppercase; letter-spacing: .05em;
}
.price-card .amount { font-size: 2.3rem; font-weight: 800; line-height: 1; margin: 12px 0 4px; }
.price-card .amount small { font-size: 0.93rem; font-weight: 500; color: var(--muted-fg); }
.price-card ul { list-style: none; margin: 18px 0; padding: 0; flex: 1 1 auto; }
.price-card li { padding: 7px 0 7px 26px; position: relative; font-size: 0.93rem; }
.price-card li::before { content: '✓'; position: absolute; left: 0; color: var(--success); font-weight: 700; }
.price-card li.off { color: var(--muted-fg); }
.price-card li.off::before { content: '×'; color: var(--muted-fg); }

/* ---------------------------------------------------------------- responsive */
@media (max-width: 1000px) { .grid-4 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 860px)  { .grid-3 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
@media (max-width: 620px)  {
  .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
  .section { padding: 40px 0; }
  h1 { font-size: 1.75rem; }
  h2 { font-size: 1.4rem; }
}

@media print {
  .site-header, .site-footer, .no-print { display: none !important; }
  body { background: #fff; }
}

/* =====================================================================
   Dashboard shell (client + admin)
   ===================================================================== */
.dash { display: grid; grid-template-columns: 232px minmax(0, 1fr); gap: 0; min-height: calc(100vh - var(--header-h)); }

.dash-side {
  background: var(--bg-alt); border-right: 1px solid var(--border);
  padding: 22px 0; position: sticky; top: var(--header-h); align-self: start;
  max-height: calc(100vh - var(--header-h)); overflow-y: auto;
}
.dash-side h5 {
  display: flex; align-items: center; gap: 7px;
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted-fg); margin: 20px 20px 8px; font-weight: 700;
}
.dash-side .dash-group:first-of-type h5 { margin-top: 0; }
/* Each group wears its colour twice: a small tile beside the heading,
   and the accent every active link inside it inherits. The eye learns
   the regions — red is the event, violet is the studio, teal is the
   day itself — without reading a single label. */
.dash-side h5 .gdot {
  width: 7px; height: 7px; border-radius: 2.5px; flex: 0 0 auto;
  background: var(--g, var(--muted-fg));
}
.dash-side a {
  display: flex; align-items: center; gap: 10px; padding: 9px 20px;
  color: var(--fg); font-size: 0.93rem; font-weight: 500; text-decoration: none;
  border-left: 3px solid transparent;
}
.dash-side .nav-ic {
  width: 17px; height: 17px; flex: 0 0 17px;
  color: var(--muted-fg); transition: color .15s ease;
}
.dash-side a:hover { background: var(--muted); text-decoration: none; }
.dash-side a:hover .nav-ic { color: var(--fg); }
.dash-side a.active {
  border-left-color: var(--g, var(--primary)); color: var(--fg);
  background: var(--primary-50);
  background: color-mix(in srgb, var(--g, var(--primary)) 10%, transparent);
  font-weight: 700;
}
.dash-side a.active .nav-ic { color: var(--g, var(--primary)); }
.dash-side .count { margin-left: auto; font-size: 0.74rem; background: var(--primary); color: #fff; border-radius: 999px; padding: 1px 7px; font-weight: 700; }

.dash-main { padding: 28px 30px 60px; min-width: 0; }
.dash-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
.dash-head h1 { font-size: 1.6rem; margin: 0 0 4px; }
.dash-head p { margin: 0; color: var(--muted-fg); font-size: 0.93rem; }

/* The Menu button and the scrim exist only on small screens. */
.dash-menu { display: none; }
.dash-scrim { display: none; }

@media (max-width: 860px) {
  .dash { grid-template-columns: 1fr; }

  /* A drawer, not a squeezed sidebar: the full grouped navigation slides
     in from the left over a scrim. It runs on the #dashNav checkbox so
     it works with scripting off; the sidebar keeps its group headings
     and the signed-in identity block, exactly as on desktop. */
  .dash-side {
    position: fixed; top: 0; bottom: 0; left: 0; z-index: 70;
    width: min(300px, 84vw); max-height: none; padding: 18px 0 30px;
    border-right: 1px solid var(--border);
    box-shadow: var(--shadow-lg);
    transform: translateX(-105%);
    transition: transform .22s ease;
  }
  .dash-nav-toggle:checked ~ .dash .dash-side { transform: translateX(0); }

  .dash-scrim {
    position: fixed; inset: 0; z-index: 60;
    background: rgba(0, 0, 0, .45);
    opacity: 0; pointer-events: none; transition: opacity .22s ease;
  }
  .dash-nav-toggle:checked ~ .dash .dash-scrim { display: block; opacity: 1; pointer-events: auto; }

  .dash-menu {
    display: inline-flex; align-items: center; gap: 8px;
    font-size: 0.86rem; font-weight: 600; color: var(--fg);
    border: 1px solid var(--border); border-radius: 99px;
    padding: 7px 16px; margin-bottom: 14px; cursor: pointer;
    background: var(--card); min-height: 40px;
  }

  /* Fingers, not cursors. */
  .dash-side a { padding: 12px 20px; min-height: 44px; }
  .dash-main { padding: 20px; }
}

/* ---- signed-in identity, pinned under the sidebar nav ----------------- */
.dash-user {
  margin: 22px 14px 0;
  padding: 12px 14px;
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 10px;
}
.dash-user .avatar {
  width: 34px; height: 34px; flex: 0 0 auto;
  border-radius: 50%;
  background: var(--primary);
  color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-weight: 800; font-size: 0.86rem;
}
.dash-user .who { min-width: 0; line-height: 1.25; }
.dash-user .who strong {
  display: block; font-size: 0.86rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dash-user .who span { font-size: 0.74rem; color: var(--muted-fg); }
.dash-user .signout-form { margin-left: auto; flex: 0 0 auto; }
.dash-user a.signout,
.dash-user button.signout {
  margin-left: auto; flex: 0 0 auto;
  font-size: 0.74rem; color: var(--muted-fg); padding: 4px;
  background: none; border: 0; cursor: pointer; font-family: inherit;
}
.dash-user a.signout:hover,
.dash-user button.signout:hover { color: var(--primary); }

/* ---- section headings ------------------------------------------------
   Every dashboard page repeated the same flex row with an inline-styled
   h2. One class instead, so they cannot drift apart. */
.section-title {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap;
  margin: 0 0 14px;
}
.section-title h2 { font-size: 1.15rem; margin: 0; }
.section-title p { margin: 2px 0 0; font-size: 0.86rem; color: var(--muted-fg); }
.section-title + .table-wrap, .section-title + .grid { margin-top: 0; }

/* stat tiles */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); gap: 14px; margin-bottom: 26px; }
.stat {
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px 18px;
  display: block;
}
.stat .label { font-size: 0.74rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted-fg); font-weight: 700; }
.stat .value { font-size: 1.7rem; font-weight: 800; line-height: 1.2; margin-top: 3px; }
.stat .value.sm { font-size: 1.3rem; }

/* A badge riding inside a table cell next to text. */
.badge-xs { font-size: 0.68rem; }
.stat .sub { font-size: 0.8rem; color: var(--muted-fg); }
.stat.accent { border-color: var(--primary); }
.stat.accent .value { color: var(--primary); }

/* A tile that leads somewhere should say so on hover. */
a.stat { color: inherit; text-decoration: none; transition: border-color .15s, box-shadow .15s, transform .15s; }
a.stat:hover {
  text-decoration: none;
  border-color: var(--primary);
  box-shadow: var(--shadow);
  transform: translateY(-1px);
}
a.stat:hover .value { color: var(--primary); }

/* Work that is actually waiting on someone, as opposed to a number that is
   merely informational. */
.stat.attention {
  border-color: var(--warning);
  background: linear-gradient(0deg, color-mix(in srgb, var(--warning) 7%, var(--card)), var(--card));
}
.stat.attention .value { color: var(--warning); }
.stat.attention .label::after {
  content: " •";
  color: var(--warning);
}

/* ---- activity feed inside a card -------------------------------------- */
.feed { display: flex; flex-direction: column; }
.feed-item { padding: 12px 22px; border-bottom: 1px solid var(--border); }
.feed-item:last-child { border-bottom: 0; }
.feed-item:hover { background: var(--bg-alt); }

/* Groups a run of tiles under a quiet caption. */
.stat-group-label {
  font-size: 0.74rem; text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted-fg); font-weight: 700;
  margin: 0 0 10px;
}

/* data tables */
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius); background: var(--card); }
table.data { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
table.data th {
  text-align: left; padding: 11px 14px; background: var(--bg-alt);
  border-bottom: 1px solid var(--border); font-size: 0.74rem;
  text-transform: uppercase; letter-spacing: .05em; color: var(--muted-fg); white-space: nowrap;
}
table.data td { padding: 12px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
table.data tr:last-child td { border-bottom: 0; }
table.data tbody tr { transition: background .12s; }
table.data tbody tr:hover { background: var(--bg-alt); }

/* Row actions stay out of the way until the row is hovered — on a long
   table a button on every row is visual noise. Always visible on touch,
   where there is no hover to reveal them. */
@media (hover: hover) and (min-width: 861px) {
  table.data tbody .row-actions .btn-ghost { opacity: 0; transition: opacity .12s; }
  table.data tbody tr:hover .row-actions .btn-ghost,
  table.data tbody tr:focus-within .row-actions .btn-ghost { opacity: 1; }
}
table.data .num { text-align: right; font-variant-numeric: tabular-nums; }
table.data .actions { white-space: nowrap; text-align: right; }
table.data .actions .btn { margin-left: 4px; }

.row-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }

/* ---- filter tabs ------------------------------------------------------ */
.tabs { display: flex; gap: 2px; flex-wrap: wrap; }
.tab {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 0.86rem; font-weight: 600;
  color: var(--muted-fg);
  border: 1px solid transparent;
}
.tab:hover { background: var(--muted); color: var(--fg); text-decoration: none; }
.tab.active {
  background: var(--primary-50);
  border-color: var(--primary);
  color: var(--primary);
}
.tab .count {
  font-size: 0.74rem; font-weight: 700;
  background: var(--muted); color: var(--muted-fg);
  border-radius: 999px; padding: 0 7px;
}
.tab.active .count { background: var(--primary); color: #fff; }

/* ---- contact message inbox -------------------------------------------- */
.msg-list {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  overflow: hidden;
}
.msg { border-bottom: 1px solid var(--border); }
.msg:last-child { border-bottom: 0; }

.msg > summary {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 2fr) auto;
  gap: 14px; align-items: center;
  padding: 13px 18px;
  cursor: pointer;
  list-style: none;
}
.msg > summary::-webkit-details-marker { display: none; }
.msg > summary:hover { background: var(--bg-alt); }
.msg[open] > summary { background: var(--bg-alt); border-bottom: 1px solid var(--border); }

/* Unread reads as unread: heavier text and a marker down the edge. */
.msg.unread > summary { box-shadow: inset 3px 0 0 var(--primary); }
.msg.unread .msg-subject { font-weight: 700; color: var(--fg); }

.msg-from { min-width: 0; display: flex; flex-direction: column; line-height: 1.3; }
.msg-from strong { font-size: 0.93rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.msg-from span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.msg-subject {
  min-width: 0; font-size: 0.93rem; color: var(--muted-fg);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.msg-meta { display: flex; align-items: center; gap: 10px; justify-content: flex-end; }

.msg-body { padding: 16px 18px 20px; }
.msg-text {
  font-size: 0.93rem; line-height: 1.6;
  background: var(--bg-alt);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
}
.msg-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }

@media (max-width: 720px) {
  .msg > summary { grid-template-columns: 1fr; gap: 6px; }
  .msg-meta { justify-content: flex-start; }
}

/* ---- uploaded asset preview in settings ------------------------------- */
.asset-preview {
  display: flex; align-items: center; gap: 14px;
  padding: 10px 12px; margin-bottom: 10px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-alt);
}
.asset-preview img {
  max-height: 54px; max-width: 160px; width: auto;
  border-radius: 4px; background: #fff; padding: 4px;
}
.asset-preview.wide img { max-width: 260px; max-height: 76px; object-fit: cover; }
.asset-preview .check { margin: 0; font-size: 0.8rem; }

/* ---- flyer design chooser -------------------------------------------- */
.tpl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
.tpl-option {
  position: relative; display: block;
  border: 2px solid var(--border); border-radius: var(--radius-sm);
  overflow: hidden; background: var(--card); color: var(--fg);
  transition: border-color .15s, transform .15s;
}
.tpl-option:hover { border-color: var(--primary); transform: translateY(-2px); text-decoration: none; }
.tpl-option.chosen { border-color: var(--primary); box-shadow: 0 0 0 3px var(--ring); }
.tpl-option img, .tpl-blank { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
.tpl-blank { background: linear-gradient(135deg, var(--secondary), var(--primary)); }
.tpl-name { display: block; padding: 7px 8px; font-size: 0.8rem; text-align: center; }
.tpl-tick {
  position: absolute; top: 7px; right: 7px;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--primary); color: #fff;
  display: grid; place-items: center; font-size: 0.8rem; font-weight: 700;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* ---- reuse-a-design disclosure ---------------------------------------- */
.reuse { margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--border); }
.reuse > summary { cursor: pointer; color: var(--muted-fg); }
.reuse > summary:hover { color: var(--primary); }
.reuse select { max-width: 210px; }

/* ---- flyer preview before generating ---------------------------------- */
.preview-wrap { position: relative; max-width: 460px; margin: 0 auto; }
.preview-img {
  width: 100%; border-radius: var(--radius);
  box-shadow: var(--shadow-lg); border: 1px solid var(--border);
}
.preview-tag {
  position: absolute; top: 12px; left: 12px;
  background: rgba(0,0,0,.62); color: #fff;
  font-size: 0.68rem; font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; padding: 4px 10px; border-radius: 999px;
  backdrop-filter: blur(3px);
}

/* ---- storage usage breakdown ------------------------------------------ */
.storage-bar {
  display: flex; height: 12px; border-radius: 999px; overflow: hidden;
  background: var(--muted); margin-bottom: 10px;
}
.storage-bar .seg { display: block; height: 100%; }

.storage-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 0.8rem; }
.legend-item { display: inline-flex; align-items: center; gap: 6px; }
.legend-item .dot { width: 10px; height: 10px; border-radius: 3px; flex: 0 0 auto; }

/* One colour per category, so the bar and legend agree. */
.seg-flyers      { background: #c92737; }
.seg-photos      { background: #2d5f7c; }
.seg-misc        { background: #6b6880; }
.seg-logos       { background: #12a05e; }
.seg-blog        { background: #d08700; }
.seg-backgrounds { background: #7c5a2d; }
.seg-heroes      { background: #3f6b4a; }
.seg-banners     { background: #6b3f5e; }
.seg-thumbs      { background: #9b98ab; }
.seg-qrcodes     { background: #3d4b7c; }
.seg-tickets     { background: #b3202f; }
.seg-previews    { background: #c9c6d4; }

/* ---- custom field definitions in the designer ------------------------- */
.cf-panel > summary {
  cursor: pointer; list-style: none;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
}
.cf-panel > summary::-webkit-details-marker { display: none; }
.cf-panel > summary::after { content: '+'; color: var(--muted-fg); font-weight: 700; }
.cf-panel[open] > summary::after { content: '−'; }

.cf-row {
  padding: 14px 0;
  border-bottom: 1px dashed var(--border);
}
.cf-row:last-child { border-bottom: 0; padding-bottom: 0; }
.cf-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }

/* ---- payment gateway cards -------------------------------------------- */
.gw > details > summary { cursor: pointer; list-style: none; }
.gw > details > summary::-webkit-details-marker { display: none; }
.gw-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
.gw-name { display: inline-flex; align-items: center; gap: 8px; }
.gw-blurb { font-weight: 400; max-width: 46ch; text-align: right; }
.gw-live { border-color: var(--success); }
.gw-meta { display: flex; flex-wrap: wrap; gap: 6px 18px; align-items: center; }
.gw-actions { display: flex; gap: 8px; flex-wrap: wrap; padding-top: 14px; border-top: 1px solid var(--border); }

@media (max-width: 720px) { .gw-blurb { text-align: left; } }

/* ---- payment method choice at checkout -------------------------------- */
.pay-choice { border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 14px; }
.pay-option {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 8px 0; cursor: pointer; font-size: 0.93rem;
}
.pay-option + .pay-option { border-top: 1px solid var(--border); }
.pay-option input { margin-top: 3px; flex: 0 0 auto; width: auto; }
.pay-option strong { display: block; }

/* ---- platform gateway allowlist --------------------------------------- */
.gw-allow { display: grid; gap: 2px; }
.gw-allow-row { padding: 9px 10px; border-radius: var(--radius-sm); align-items: flex-start; margin: 0; }
.gw-allow-row:hover { background: var(--bg-alt); }
.gw-allow-row input[disabled] + span { opacity: .55; }
.gw-allow-row strong { margin-right: 6px; }

/* toolbar above tables */
.toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 16px; }
.toolbar .field { margin: 0; }
.toolbar .field input, .toolbar .field select { min-width: 160px; }

/* form sections */
.form-section { margin-bottom: 26px; }
.form-section > h3 { font-size: 1.05rem; padding-bottom: 8px; border-bottom: 1px solid var(--border); margin-bottom: 16px; }

.help { font-size: 0.8rem; color: var(--muted-fg); margin-top: 4px; }

.sticky-actions {
  position: sticky; bottom: 0; background: var(--bg); border-top: 1px solid var(--border);
  padding: 14px 0; margin-top: 24px; display: flex; gap: 10px; flex-wrap: wrap; z-index: 10;
}

/* image preview */
.img-preview {
  width: 100%; max-width: 260px; aspect-ratio: 16/9; border: 1px dashed var(--border);
  border-radius: var(--radius-sm); background: var(--muted); overflow: hidden;
  display: grid; place-items: center; color: var(--muted-fg); font-size: 0.8rem; margin-bottom: 8px;
}
.img-preview img { width: 100%; height: 100%; object-fit: cover; }

/* =====================================================================
   Utilities
   Consolidated from repeated inline styles so the same intent always
   produces the same result, and both themes stay in step.
   ===================================================================== */

/* Inline forms — used wherever a single-button action sits in a table row. */
.inline-form { display: inline; }

/* Semantic text colours */
/* Text by definition, so the foreground weights. These were the fills:
   a discount line reading "-USD 75.00" in --success measured 3.38. */
.text-danger  { color: var(--danger-fg); }
.text-success { color: var(--ok); }
.text-warning { color: var(--warning-fg); }
.text-primary { color: var(--primary); }

/* A ghost button that reads as destructive without shouting */
.btn-ghost.btn-danger-text { color: var(--destructive); background: transparent; }
.btn-ghost.btn-danger-text:hover { background: rgba(239, 43, 65, .1); color: var(--destructive); }
.btn-outline.btn-danger-text { color: var(--destructive); border-color: var(--destructive); }
.btn-outline.btn-danger-text:hover { background: rgba(239, 43, 65, .08); color: var(--destructive); }

/* Codes, references and ticket numbers */
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.strike { text-decoration: line-through; }

/* A badge small enough to sit inline beside a table cell's title */
.badge-xs { font-size: 0.68rem; padding: 2px 7px; }

/* Display icons for empty states and result screens */
.icon-lg { font-size: 2.4rem; line-height: 1; }
.icon-xl { font-size: 3rem; line-height: 1; }

/* Roomier card body for confirmation and empty-state panels */
.card-body-lg { padding: 42px 28px; }
@media (max-width: 560px) { .card-body-lg { padding: 30px 20px; } }

/* Compact card body for the small stat cards above a form */
.card-body-sm { padding: 14px 18px; }

/* Images that fill their box */
.img-cover   { width: 100%; height: 100%; object-fit: cover; }
.img-contain { width: 100%; height: 100%; object-fit: contain; }

/* Consistent heading sizes inside dashboard sections */
.h-section { font-size: 1.2rem; margin: 0 0 16px; }
.h-panel   { font-size: 1.4rem; }

/* Colour swatch inputs need a usable hit area */
input[type="color"] {
  height: 44px;
  padding: 4px;
  cursor: pointer;
  max-width: 180px;
}

/* Range inputs inherit almost nothing by default */
input[type="range"] {
  width: 100%;
  accent-color: var(--primary);
  padding: 0;
  background: transparent;
  border: 0;
}
/* No ring while dragging with a mouse, which is why the rule exists.
   But it took the keyboard ring with it, and one of these is the zoom
   on the public photo portal — an attendee cropping their own badge
   photo, with nothing on screen to say where the focus went. */
input[type="range"]:focus { outline: none; box-shadow: none; }
input[type="range"]:focus-visible { outline: 3px solid var(--ring); outline-offset: 3px; }

/* File inputs — make them look like part of the system */
input[type="file"] {
  padding: 8px 10px;
  font-size: 0.86rem;
  cursor: pointer;
}
input[type="file"]::file-selector-button {
  margin-right: 12px;
  padding: 6px 13px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card);
  color: var(--fg);
  font: inherit;
  font-size: 0.86rem;
  font-weight: 600;
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover {
  border-color: var(--primary);
  color: var(--primary);
}

/* Disabled inputs should read as unavailable, not broken */
input:disabled, select:disabled, textarea:disabled {
  opacity: .6;
  cursor: not-allowed;
  background: var(--muted);
}

/* QR panels on tickets and verification screens */
.qr-box {
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background: var(--muted);
  color: var(--muted-fg);
  font-size: 0.8rem;
  text-align: center;
}

/* Anything that must never widen the page */
.scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Respect a reduced-motion preference across the whole app */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Keyboard focus must always be visible, including on links and rows */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--ring);
  outline-offset: 2px;
  border-radius: 3px;
}

/* ---------------------------------------------------- add to calendar */
/* A plain .ics download is right for Apple and desktop Outlook and wrong
   for everyone on Google or Outlook web, who get a file their phone may
   not open. The menu offers each service its own link. */
.cal-menu { position: relative; }
.cal-menu > summary { list-style: none; cursor: pointer; }
.cal-menu > summary::-webkit-details-marker { display: none; }
.cal-options {
  /* In flow, not absolutely positioned.
     It used to float over the page, which meant the nearest .card — which
     carries overflow:hidden for its rounded corners — simply clipped it
     away. The menu was rendering at full size and being cut off, so it
     read as a button that did nothing.
     Three items in a sidebar card do not need an overlay; letting them
     push the card open is immune to clipping, needs no z-index, and
     behaves on a phone where an overlay would run off-screen. */
  margin-top: 6px;
  background: var(--bg-alt); border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

/* A hint that the panel belongs to the button above it. */
.cal-menu[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
.cal-options a {
  display: block; padding: 10px 14px; font-size: 0.86rem;
  color: var(--fg); text-decoration: none;
}
.cal-options a + a { border-top: 1px solid var(--border); }
.cal-options a:hover { background: var(--bg-alt); color: var(--primary); }
.cal-options .tiny { display: block; }


/* ---- glass and display type ------------------------------------------
   Restraint is the point. Frosting everything reads as a novelty; the
   header and a few raised surfaces read as considered. Each rule has an
   opaque fallback first, because backdrop-filter is unsupported in a
   handful of browsers and an unsupported translucent panel is unreadable
   rather than merely plain. */

.glass,
.site-header {
  background: var(--bg);                      /* fallback, applied first */
  background: var(--glass-bg);
  border-bottom: 1px solid var(--glass-line);
  box-shadow: var(--glass-shadow);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .glass,
  .site-header {
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%);
    backdrop-filter: blur(var(--glass-blur)) saturate(180%);
  }
}

.site-header { position: sticky; top: 0; z-index: 50; }

/* A raised panel: same material, all four sides. */
.glass-card {
  background: var(--bg);
  background: var(--glass-bg);
  border: 1px solid var(--glass-line);
  border-radius: 16px;
  box-shadow: var(--glass-shadow);
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .glass-card {
    -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%);
    backdrop-filter: blur(var(--glass-blur)) saturate(180%);
  }
}

/* Manrope's display voice. No optical axis to drive — its wide
   apertures and squared terminals already read as designed at size;
   what it wants is firmer weight and a touch less tracking than
   Inter needed. */
h1, h2, h3,
.display {
  letter-spacing: -0.015em;
  font-weight: 800;
}

h1, .display { letter-spacing: -0.022em; line-height: 1.1; }
h2 { letter-spacing: -0.018em; font-weight: 700; }
h3 { font-weight: 700; }

/* Tabular figures wherever numbers are compared down a column, so digits
   line up instead of shimmying. */
.data td.num, .data th.num,
.stat .value, .rtile .v, .runpct {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* Manrope carries no optical axis; body copy needs no coaxing. */

/* ======================================================================
   LAYOUT UPGRADE
   ----------------------------------------------------------------------
   The front end was built page by page, and it showed: every section
   carried the same 56px of padding, so eight consecutive blocks all
   weighed exactly the same. A page with no variation in density has no
   hierarchy — the eye finds no entry point and reads everything, or
   nothing. What follows is a system rather than a set of tweaks.

   Four ideas, in order of how much they matter:

   1. RHYTHM. Sections now come in three weights. A hero breathes, a
      major section is generous, a minor one is tight. Adjacent sections
      of the same tone collapse their shared padding so two related
      blocks read as one region instead of two.

   2. MEASURE. Prose is capped at 68 characters. The container is
      1200px, which is right for a grid of cards and much too wide for a
      paragraph — at that width the eye loses the start of the next line.

   3. ELEVATION. One shadow scale, used consistently, so raised things
      are raised by the same amount and depth means something.

   4. RESTRAINT IN MOTION. Hover lifts by 2px and changes the border.
      Nothing scales — scaling resamples text and makes it blur, which
      reads as cheap however smooth the easing.
   ====================================================================== */

:root {
  /* A 4px base. Named for role, not size, so a change of scale does not
     mean renaming every use. */
  --space-2xs: .25rem;   /* 4  */
  --space-xs:  .5rem;    /* 8  */
  --space-sm:  .75rem;   /* 12 */
  --space-md:  1rem;     /* 16 */
  --space-lg:  1.5rem;   /* 24 */
  --space-xl:  2.5rem;   /* 40 */
  --space-2xl: 4rem;     /* 64 */
  --space-3xl: 6rem;     /* 96 */

  /* Elevation. Two layers each: a tight shadow for the edge, a soft one
     for the cast. A single blurred shadow always looks like a sticker. */
  --lift-1: 0 1px 2px rgba(16,24,40,.06), 0 1px 3px rgba(16,24,40,.10);
  --lift-2: 0 2px 4px rgba(16,24,40,.06), 0 4px 12px rgba(16,24,40,.10);
  --lift-3: 0 4px 8px rgba(16,24,40,.06), 0 12px 28px rgba(16,24,40,.14);

  --measure: 68ch;
}

[data-theme="dark"] {
  --lift-1: 0 1px 2px rgba(0,0,0,.30), 0 1px 3px rgba(0,0,0,.40);
  --lift-2: 0 2px 4px rgba(0,0,0,.30), 0 4px 12px rgba(0,0,0,.44);
  --lift-3: 0 4px 8px rgba(0,0,0,.34), 0 12px 28px rgba(0,0,0,.52);
}

/* ---- section rhythm -------------------------------------------------- */

.section          { padding-block: var(--space-2xl); }
.section-sm       { padding-block: var(--space-xl); }
.section-lg       { padding-block: var(--space-3xl); }

/* Two sections of the same tone in a row are one region, not two.
   Collapsing the seam is what stops a long page reading as a list. */
.section-alt + .section-alt,
.section:not(.section-alt) + .section:not(.section-alt) { padding-top: 0; }

/* ---- measure --------------------------------------------------------- */

.section p:not([class]),
.prose,
.lede { max-width: var(--measure); }

.lede {
  font-size: clamp(1.05rem, 1.6vw, 1.2rem);
  line-height: 1.6;
  color: var(--muted-fg);
}

/* ---- section headers ------------------------------------------------- */
/* Eyebrow, title, one line of support. The same three parts every time,
   so a reader scanning the page learns the shape once. */

.sec-head { margin-bottom: var(--space-xl); max-width: var(--measure); }
.sec-head .eyebrow {
  display: inline-block; margin-bottom: var(--space-xs);
  font-size: 0.74rem; font-weight: 600; letter-spacing: .08em;
  text-transform: uppercase; color: var(--primary);
}
.sec-head h2 { margin: 0 0 var(--space-sm); font-size: clamp(1.6rem, 3.2vw, 2.25rem); }
.sec-head p  { margin: 0; color: var(--muted-fg); font-size: 1rem; line-height: 1.6; }
.sec-head.center { margin-inline: auto; text-align: center; }

/* ---- cards ----------------------------------------------------------- */

.card, .event-card {
  border-radius: 14px;
  box-shadow: var(--lift-1);
  transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease;
}

a.event-card:hover, .event-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--lift-3);
  border-color: color-mix(in srgb, var(--primary) 32%, var(--border));
}

/* Images get a fixed ratio so a grid of cards has a straight top edge
   whatever shape the uploads are. Ragged card tops are the single most
   common reason a card grid looks unfinished. */
.event-card .thumb {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--bg-alt);
}
.event-card .thumb img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .4s cubic-bezier(.22,.61,.36,1);
}
.event-card:hover .thumb img { transform: scale(1.03); }

/* ---- grids ----------------------------------------------------------- */
/* auto-fit with a floor, so the layout answers the space it is given
   rather than to a handful of hard breakpoints. */

.grid-auto  { display: grid; gap: var(--space-lg); grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); }
.grid-cards { display: grid; gap: var(--space-lg); grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); }

/* ---- hero ------------------------------------------------------------ */

.hero { padding-block: clamp(var(--space-2xl), 9vw, var(--space-3xl)); }
.hero h1 { font-size: clamp(2.1rem, 5.4vw, 3.6rem); margin-bottom: var(--space-md); }
.hero .lede { margin-bottom: var(--space-xl); }

/* ---- focus ----------------------------------------------------------- */
/* :focus-visible, so a mouse click does not draw a ring but a Tab key
   does. Removing focus rings outright is the most common accessibility
   failure on a marketing site. */

a:focus-visible, button:focus-visible,
input:focus-visible, select:focus-visible,
textarea:focus-visible, summary:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ---- motion ---------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---- small screens --------------------------------------------------- */

@media (max-width: 640px) {
  .section    { padding-block: var(--space-xl); }
  .section-lg { padding-block: var(--space-2xl); }
  .sec-head   { margin-bottom: var(--space-lg); }
}

/* ------------------------------------------------ touch and small screens

   Two rules here, and the first one is a bug fix rather than a polish.

   iOS Safari zooms the viewport whenever a form field with a font size
   under 16px takes focus, and it does not zoom back out. Every input on
   this platform was .93rem — 14.88px — so tapping any field on an
   iPhone left the page magnified and wider than the screen for the rest
   of the visit. That is what "the checkout is too wide on mobile" was:
   not the layout, which fits, but what happens the moment somebody taps
   a name field. It affected every form on the site, so the fix belongs
   here rather than on one page.

   16px exactly. Anything larger is a design change; anything smaller
   brings the zoom back.

   The second is target size. A 40px control is hittable with a mouse and
   marginal with a thumb, and the checkout is the worst place to make
   somebody tap twice. */
@media (max-width: 860px) {
  .input,
  input[type=text], input[type=email], input[type=password], input[type=number],
  input[type=date], input[type=time], input[type=url], input[type=tel],
  input[type=search], input[type=datetime-local],
  select, textarea {
    font-size: 16px;
    min-height: 46px;
    padding: 11px 14px;
  }

  select { padding-right: 36px; }

  textarea { min-height: 120px; }

  /* Anything a thumb has to find. Small buttons stay visually small but
     gain the height they need to be hit reliably. */
  .btn { min-height: 46px; padding-inline: 20px; }
  .btn-sm { min-height: 40px; font-size: 0.86rem; }

  /* Checkboxes and radios are drawn at their intrinsic size, so the
     label carries the target instead. */
  label.check { min-height: 44px; display: flex; align-items: center; gap: 10px; }

  /* A summary panel pinned below a single column of form has nothing to
     stay beside — it just covers the thing underneath it as you scroll. */
  .sidebar-sticky { position: static; top: auto; }

  /* The menu toggle is the most-tapped control on a phone and was the
     smallest thing on the page. */
  .nav-toggle, .theme-toggle { width: 44px; height: 44px; }
}

/* ------------------------------------------- editable order summary line */
.sum-line { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px;
            align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); }
.sum-line:last-of-type { border-bottom: 0; }
.sum-what b { display: block; font-size: 0.93rem; }
.sum-each { font-size: 0.74rem; color: var(--muted-fg); }
.sum-qty { grid-column: 1; display: flex; align-items: center; gap: 6px; }
.sum-qty select { width: 74px; }
.sum-money { grid-column: 2; grid-row: 1 / span 2; align-self: center;
             font-weight: 600; font-size: 0.93rem; font-variant-numeric: tabular-nums; }
.sum-remove { background: none; border: 0; cursor: pointer; line-height: 1;
              font-size: 1.3rem; color: var(--muted-fg); padding: 4px 8px;
              border-radius: var(--radius-sm); }
.sum-remove:hover { color: var(--destructive); background: var(--bg-alt); }
.sum-remove:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* Screen-reader-only, for labels the layout does not have room to show. */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0;
                   margin: -1px; overflow: hidden; clip: rect(0 0 0 0);
                   white-space: nowrap; border: 0; }

@media (max-width: 860px) { .sum-remove { min-width: 44px; min-height: 44px; } }

/* One attendee, and the control to drop them.

   The remove was first written as a float, which put it in no layout at
   all: the grid of fields that follows does not reserve space for a
   float, so the second block's button ended up underneath a field. It is
   anchored to the fieldset instead, level with the legend, where there
   is always room for it.

   formnovalidate on the button is what makes it work at all. The sibling
   blocks are required fields, so without it the browser refuses to
   submit and the buyer is stuck with a row they cannot delete. */
.att-block { position: relative; border: 1px solid var(--border);
             border-radius: var(--radius-sm); padding: 16px; margin-bottom: 14px; }
.att-block > legend { padding: 0 8px; }

.att-remove {
  position: absolute; top: -13px; right: 12px; z-index: 1;
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--card); border: 1px solid var(--border);
  border-radius: 999px; padding: 3px 12px; cursor: pointer;
  font-size: 0.74rem; font-weight: 600; color: var(--muted-fg); line-height: 1.6;
}
.att-remove .x { font-size: 1rem; line-height: 1; }
.att-remove:hover { color: var(--destructive); border-color: var(--destructive); }
.att-remove:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

@media (max-width: 860px) {
  /* Room for the control above the legend rather than beside it, since
     a long ticket name and a button do not share 375px. */
  .att-block { padding-top: 22px; }
  .att-remove { min-height: 36px; padding: 6px 14px; }
}

/* ===================================================================
   Sponsors

   One rule governs the lot: a logo is never distorted. Every tile sizes
   its box and lets the image sit inside with object-fit: contain, so a
   wide wordmark and a square badge both arrive whole. Nothing sets both
   width and height on an img.

   Sizing is in clamp() rather than at breakpoints, so a logo shrinks
   smoothly on a narrow phone instead of snapping between two sizes.
   =================================================================== */

.sp-tile { display: grid; place-items: center; padding: 10px 14px;
           border: 1px solid var(--border); border-radius: 10px;
           background: var(--card); min-height: 72px; text-decoration: none; }
.sp-tile img { max-width: 100%; max-height: 46px; width: auto; height: auto;
               object-fit: contain; }
.sp-tile .sp-name { font-size: 0.86rem; font-weight: 600; color: var(--fg);
                    text-align: center; }
a.sp-tile { transition: border-color .15s ease, transform .15s ease; }
a.sp-tile:hover { border-color: var(--primary); transform: translateY(-1px); }
a.sp-tile:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }

.sp-tile-sm { min-height: 56px; padding: 8px 12px; }
.sp-tile-sm img { max-height: 32px; }
.sp-tile-lg { min-height: clamp(96px, 22vw, 140px); padding: 18px 26px; }
.sp-tile-lg img { max-height: clamp(56px, 14vw, 88px); }

/* auto-fit means the columns are decided by the space available, so this
   is one rule for phone, tablet and desktop rather than three. */
.sp-grid { display: grid; gap: 14px;
           grid-template-columns: repeat(auto-fit, minmax(clamp(120px, 30vw, 170px), 1fr)); }
.sp-grid-sm { grid-template-columns: repeat(auto-fit, minmax(clamp(100px, 24vw, 130px), 1fr)); }

.sp-tier { margin-bottom: 22px; }
.sp-tier:last-child { margin-bottom: 0; }
.sp-tier-head { font-size: 0.74rem; letter-spacing: .1em; text-transform: uppercase;
                color: var(--muted-fg); font-weight: 700; margin: 0 0 10px; }

/* Tier is expressed as size, which is what prominence actually is — not
   as a louder colour.

   The scale arrives as a custom property set per group, derived from the
   tier's position in the organiser's own list. Naming the tiers in CSS
   would have meant only five names could ever work, and an organiser who
   sells Diamond and Knowledge Partner would get no hierarchy at all. */
.sp-tier { --sp-scale: 1; }
.sp-tier .sp-tile { min-height: calc(66px * var(--sp-scale) + 30px); }
.sp-tier .sp-tile img { max-height: calc(46px * var(--sp-scale) + 18px); }
.sp-tier .sp-grid {
  grid-template-columns: repeat(auto-fit,
    minmax(clamp(110px, calc(20vw * var(--sp-scale) + 8vw), calc(120px * var(--sp-scale) + 60px)), 1fr));
}

.sp-featured { margin-bottom: 18px; }
.sp-featured-row { display: grid; gap: 14px;
                   grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); }

.sp-cards { display: grid; gap: 14px;
            grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr)); }
.sp-card { border: 1px solid var(--border); border-radius: 12px; overflow: hidden;
           background: var(--card); display: flex; flex-direction: column; }
.sp-card .sp-tile { border: 0; border-bottom: 1px solid var(--border);
                    border-radius: 0; background: var(--bg-alt); min-height: 96px; }
.sp-card-body { padding: 12px 14px; }
.sp-card-body b { display: block; font-size: 0.93rem; }
.sp-card-tier { font-size: 0.68rem; letter-spacing: .07em; text-transform: uppercase;
                color: var(--muted-fg); }
.sp-card-body p { margin: 6px 0 0; font-size: 0.86rem; color: var(--muted-fg); }

/* A strip scrolls sideways rather than wrapping, so a long list stays
   one band on a phone. The page itself never scrolls sideways. */
.sp-strip { min-width: 0; display: flex; gap: 12px; overflow-x: auto; padding-bottom: 4px;
            scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; }
.sp-strip .sp-tile { flex: 0 0 auto; min-width: 132px; scroll-snap-align: start; }

.sp-marquee { min-width: 0; overflow: hidden; }
.sp-marquee-track { display: flex; gap: 12px; width: max-content;
                    animation: sp-scroll 38s linear infinite; }
.sp-marquee-copy { display: flex; gap: 12px; }
.sp-marquee:hover .sp-marquee-track,
.sp-marquee:focus-within .sp-marquee-track { animation-play-state: paused; }
.sp-marquee .sp-tile { flex: 0 0 auto; min-width: 132px; }
@keyframes sp-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }

.sp-compact { margin-bottom: 14px; }
.sp-compact-tier { font-size: 0.68rem; letter-spacing: .09em; text-transform: uppercase;
                   color: var(--muted-fg); font-weight: 700; }
.sp-compact ul { list-style: none; margin: 4px 0 0; padding: 0;
                 display: flex; flex-wrap: wrap; gap: 4px 16px; }
.sp-compact li { font-size: 0.93rem; }

/* Motion is a preference, not a decoration. A moving strip becomes a
   still one that can still be scrolled by hand. */
@media (prefers-reduced-motion: reduce) {
  .sp-marquee-track { animation: none; }
  .sp-marquee { overflow-x: auto; }
  a.sp-tile:hover { transform: none; }
}

/* Sponsor placement slots. The band is a quiet full-width strip so the
   top and bottom placements read as a separate register from the page
   content rather than as another section competing with it. */
.sp-band { background: var(--bg-alt); border-block: 1px solid var(--border); }
.sp-slot-head { font-size: 1rem; margin: 0 0 12px; }
.sp-slot-sidebar .sp-slot-head { font-size: 0.8rem; letter-spacing: .08em;
                                 text-transform: uppercase; color: var(--muted-fg); }
/* The side column is narrow, so whatever layout was chosen collapses to
   something that fits rather than overflowing it. */
.sp-slot-sidebar .sp-grid { grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); gap: 8px; }
.sp-slot-sidebar .sp-tile { min-height: 52px; padding: 6px 8px; }
.sp-slot-sidebar .sp-tile img { max-height: 30px; }
.sp-slot-sidebar .sp-cards { grid-template-columns: 1fr; }

/* ------------------------------------------------- description editor */
.rt { border: 1px solid var(--border); border-radius: var(--radius-sm);
      background: var(--input-bg); overflow: hidden; }
.rt-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 2px;
          padding: 6px 8px; border-bottom: 1px solid var(--border);
          background: var(--bg-alt); }
.rt-bar button { background: none; border: 1px solid transparent; cursor: pointer;
                 font-size: 0.8rem; color: var(--fg); padding: 5px 9px;
                 border-radius: 6px; line-height: 1.2; min-width: 32px; }
.rt-bar button:hover { background: var(--card); border-color: var(--border); }
.rt-bar button:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
.rt-sep { width: 1px; height: 18px; background: var(--border); margin: 0 4px; }

.rt-area { min-height: 190px; padding: 12px 14px; font-size: 0.93rem;
           line-height: 1.65; color: var(--fg); overflow-y: auto; max-height: 460px; }
.rt-area:focus { outline: none; }
.rt:focus-within { border-color: var(--primary); }
.rt-area.input-error { background: transparent; }
.rt-area p { margin: 0 0 10px; }
.rt-area p:last-child { margin-bottom: 0; }
.rt-area ul, .rt-area ol { margin: 0 0 10px; padding-left: 22px; }
.rt-area blockquote { margin: 0 0 10px; padding-left: 12px;
                      border-left: 3px solid var(--border); color: var(--muted-fg); }
.rt-area a { color: var(--primary); }

/* Without JavaScript the toolbar cannot work, so it is not shown and the
   plain textarea is what the person gets. The script adds .rt-on, which
   is what flips them — so the fallback is the default state rather than
   something that depends on the script having failed politely. */
.rt-bar, .rt-area { display: none; }
.rt textarea { display: block; width: 100%; min-height: 190px; border: 0;
               background: transparent; padding: 12px 14px; resize: vertical;
               font-family: inherit; }
.rt-on .rt-bar { display: flex; }
.rt-on .rt-area { display: block; }
.rt-on textarea { display: none; }

@media (max-width: 860px) {
  .rt-bar button { min-height: 40px; }
  .rt-area { font-size: 16px; }
}

/* =====================================================================
   Shared components, phase two of the redesign.

   These existed before — as fifty-seven private copies inside page
   <style> blocks. Each pattern below is the canonical recipe, with the
   page-local class names kept as aliases so no markup had to change to
   adopt it. New pages use the canonical names; old pages shed their
   local copies without moving a single tag.
   ===================================================================== */

:root { --info: #2f6fd6; }
html[data-theme="dark"] { --info: #6ea8ff; }

/* ---- chips: one small statement of state ---------------------------
   This is the only pill in the system. Six screens had each grown their
   own — .an-state, .ct-state, .rv-pill, .au-state, .doc-state,
   .doc-status — between them five font sizes, five paddings, one with
   no border at all and two with a dot. They are all this, now.

   The tones are the foreground weights: a chip is text, and --success
   and --warning are accents that carry only 3.38 and 2.94 against a
   card, under the 4.5 small text owes. --primary was worse — legible on
   white at 5.48, but 3.28 on a dark card, so the attention chip was
   failing in dark mode wherever it appeared. */
.chip, .sp-state, .fm-chip, .spk-deadline {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 0.68rem; text-transform: uppercase;
  letter-spacing: .06em; border: 1px solid var(--border); border-radius: 99px;
  padding: 1px 9px; color: var(--muted-fg); vertical-align: middle;
  white-space: nowrap;
}

/* A dot, for the screens that led with one. It takes the chip's own
   tone, so a dot can never disagree with the word beside it. */
.chip > i { width: 7px; height: 7px; border-radius: 50%; flex: none;
            background: currentColor; }

/* The larger register: one standing status on a record or a public
   page, where the chip is the answer rather than a cell in a list and
   the label is a sentence — so it is not shouted. */
.chip-lead { font-size: 0.8rem; padding: 4px 12px;
             text-transform: none; letter-spacing: 0; }

.chip-ok,        .sp-state-ok,   .fm-chip.on { border-color: var(--ok); color: var(--ok); }
.chip-warn,      .sp-state-warn, .fm-chip.lock { border-color: var(--warning-fg); color: var(--warning-fg); }
.chip-danger    { border-color: var(--danger-fg); color: var(--danger-fg); }
.chip-info      { border-color: var(--info); color: var(--info); }
.chip-attention, .sp-state-attention { border-color: var(--primary-fg); color: var(--primary-fg); font-weight: 700; }

/* The words the data already uses, so a screen can keep interpolating
   its own status straight into the class — class="chip <?= $status ?>"
   — instead of every page inventing a mapping. Anything not named here
   stays neutral, which is the right answer for a state nobody has
   decided the meaning of yet. */
.chip.sent, .chip.published, .chip.accepted, .chip.yes, .chip.on, .chip.ok
  { border-color: var(--ok); color: var(--ok); }
.chip.draft, .chip.wait, .chip.dup, .chip.pending
  { border-color: var(--warning-fg); color: var(--warning-fg); }
.chip.rejected, .chip.no, .chip.off, .chip.failed
  { border-color: var(--danger-fg); color: var(--danger-fg); }

/* ---- step indicators: wizards say where you are --------------------- */
.steps, .im-steps, .mc-steps { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
.step, .im-step, .mc-step {
  font-size: 0.8rem; padding: 4px 12px; border-radius: 99px;
  border: 1px solid var(--border); color: var(--muted-fg);
}
/* Text again, so the foreground weights — the same two the chips had
   wrong. "You are here" in a wizard is the one label on the screen a
   reader must not lose, and on a dark card --primary reached 3.28. */
.step.on,   .im-step.on,   .mc-step.on   { border-color: var(--primary-fg); color: var(--primary-fg); font-weight: 600; }
.step.done, .im-step.done, .mc-step.done { border-color: var(--ok); color: var(--ok); }

/* ---- stat tiles: a number with its name ----------------------------- */
.stat-grid, .im-tally {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); margin-bottom: 14px;
}
.stat, .im-num { border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; }
.stat b, .im-num b { display: block; font-size: 1.6rem; line-height: 1.1; font-variant-numeric: tabular-nums; }
.stat span, .im-num span { font-size: 0.74rem; color: var(--muted-fg); }
.stat.good, .im-num.good { border-color: var(--success); }
.stat.bad,  .im-num.bad  { border-color: var(--destructive); }

/* ---- tabs: one page, several jobs ----------------------------------- */
.tabs, .sp-tabs { display: flex; gap: 6px; flex-wrap: wrap; border-bottom: 1px solid var(--border); }
.tab, .sp-tab {
  padding: 8px 16px; font-size: 0.86rem; color: var(--muted-fg); text-decoration: none;
  border: 1px solid transparent; border-bottom: 0;
  border-radius: 10px 10px 0 0; margin-bottom: -1px;
}
.tab:hover, .sp-tab:hover { color: var(--fg); text-decoration: none; }
.tab.on, .sp-tab.on { color: var(--fg); font-weight: 600; border-color: var(--border); background: var(--card); }

/* ---- toolbar: a row of actions with one line of context ------------- */
.toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 18px; }
.toolbar .hint { flex: 1 1 260px; }

/* ---- confirm dialogs ------------------------------------------------- */
dialog.confirm {
  border: 1px solid var(--border); border-radius: 12px; padding: 0;
  max-width: 460px; width: calc(100% - 32px);
  background: var(--card); color: var(--fg);
}
dialog.confirm::backdrop { background: rgba(0, 0, 0, .5); }
dialog.confirm .card-body { padding: 22px 24px; }

/* ---- loading: buttons that admit they are working -------------------- */
.btn.loading { position: relative; color: transparent !important; pointer-events: none; }
.btn.loading::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  width: 16px; height: 16px; border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .4); border-top-color: #fff;
  animation: btn-spin .7s linear infinite;
}
.btn-outline.loading::after, .btn-ghost.loading::after {
  border-color: var(--border); border-top-color: var(--muted-fg);
}
@keyframes btn-spin { to { transform: rotate(360deg); } }

.skeleton {
  border-radius: var(--radius-sm); background: var(--muted);
  background-image: linear-gradient(90deg, transparent, rgba(255, 255, 255, .35), transparent);
  background-size: 200% 100%; animation: skeleton-sweep 1.2s ease-in-out infinite;
  color: transparent; min-height: 1em;
}
@keyframes skeleton-sweep { from { background-position: 200% 0; } to { background-position: -200% 0; } }

/* ---- tables that become cards on a phone ----------------------------
   Opt in with class="data data-cards" plus data-th="Column name" on
   each td. Desktop is untouched; under 700px each row becomes a small
   card and every cell carries its own heading. */
@media (max-width: 700px) {
  table.data.data-cards thead { display: none; }
  table.data.data-cards, table.data.data-cards tbody,
  table.data.data-cards tr, table.data.data-cards td { display: block; width: 100%; }
  table.data.data-cards tr {
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    padding: 10px 14px; margin-bottom: 10px;
  }
  table.data.data-cards td { border: 0; padding: 4px 0; }
  table.data.data-cards td[data-th]::before {
    content: attr(data-th); display: block; font-size: 0.68rem;
    text-transform: uppercase; letter-spacing: .06em; color: var(--muted-fg);
  }
}

/* ------------------------------------------------------------- charts
   Server-rendered SVG and bar markup from lib/charts.php. The marks
   stay thin, the grid stays recessive, and every value is ink — the
   series colour carries only the shape. */
.cts { display: block; width: 100%; height: auto; }
.cts-grid { stroke: var(--border); stroke-width: 1; }
.cts-ylab, .cts-xlab {
  fill: var(--muted-fg); font-size: 10.5px;
  font-variant-numeric: tabular-nums;
}
.cts-ylab { text-anchor: end; }
.cts-area { fill: var(--primary); opacity: .12; }
.cts-line { fill: none; stroke: var(--primary); stroke-width: 2;
            stroke-linecap: round; stroke-linejoin: round; }
.cts-end  { fill: var(--primary); }
.cts-pt circle, .cts-pt text { opacity: 0; transition: opacity .1s ease; pointer-events: none; }
.cts-pt circle { fill: var(--primary); stroke: var(--card); stroke-width: 2; }
.cts-pt text {
  fill: var(--muted-fg); font-size: 10.5px;
  paint-order: stroke; stroke: var(--card); stroke-width: 3.5px; stroke-linejoin: round;
}
.cts-pt text.v { fill: var(--fg); font-weight: 700; font-size: 12px;
                 font-variant-numeric: tabular-nums; }
.cts-pt:hover circle, .cts-pt:hover text { opacity: 1; }

/* --------------------------------------------------- event workspace
   One event's production office: a hero strip carrying its artwork,
   the pulse band, and a grid of department doors. */
.ws-hero { height: 150px; border-radius: var(--radius); background: var(--muted) center/cover;
           margin-bottom: 16px; position: relative; overflow: hidden; }
.ws-hero.has-img { background-size: cover; }
.ws-hero-scrim { position: absolute; inset: 0; display: flex; gap: 8px;
                 align-items: flex-start; justify-content: flex-end; padding: 12px 14px;
                 background: linear-gradient(180deg, rgba(31,22,41,.28), transparent 55%); }
@media (max-width: 640px) { .ws-hero { height: 110px; } }

.ws-areas { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 12px; }
.ws-area { display: flex; gap: 12px; align-items: center; padding: 14px 15px;
           background: var(--card); border: 1px solid var(--border); border-radius: 12px;
           text-decoration: none; color: inherit;
           transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease; }
.ws-area:hover { transform: translateY(-2px); border-color: var(--primary);
                 box-shadow: 0 8px 22px rgba(31,22,41,.10); text-decoration: none; }
.ws-ic { width: 38px; height: 38px; border-radius: 10px; flex: 0 0 auto;
         display: inline-flex; align-items: center; justify-content: center;
         background: var(--primary-50); color: var(--primary); }
.ws-tx { min-width: 0; flex: 1 1 auto; }
.ws-tx b { display: block; font-size: 0.93rem; }
.ws-tx span { display: block; font-size: 0.74rem; color: var(--muted-fg); line-height: 1.35; }
.ws-area .chev { color: var(--muted-fg); font-size: 1.1rem; }

/* The quiet three-dot menu on a card: a details element, so it needs
   no script and closes itself when another one opens focus. */
.more { position: relative; margin-left: auto; }
.more > summary { list-style: none; cursor: pointer; width: 32px; height: 32px;
                  display: inline-flex; align-items: center; justify-content: center;
                  border-radius: 8px; border: 1px solid var(--border);
                  color: var(--muted-fg); font-weight: 700; }
.more > summary::-webkit-details-marker { display: none; }
.more > summary:hover { color: var(--fg); border-color: var(--fg); }
.more[open] > summary { border-color: var(--primary); color: var(--primary); }
.more-panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
              min-width: 185px; background: var(--card); border: 1px solid var(--border);
              border-radius: 10px; box-shadow: 0 12px 32px rgba(31,22,41,.16);
              padding: 6px; display: grid; }
.more-panel a { padding: 8px 11px; border-radius: 7px; font-size: 0.86rem;
                color: var(--fg); text-decoration: none; }
.more-panel a:hover { background: var(--muted); text-decoration: none; }

.ev-tally { display: flex; gap: 14px; flex-wrap: wrap; font-size: 0.8rem;
            color: var(--muted-fg); font-variant-numeric: tabular-nums; }
.ev-tally b { color: var(--fg); font-weight: 700; }

.hbars { display: grid; gap: 9px; }
.hb { display: grid; grid-template-columns: minmax(90px, 1fr) minmax(0, 2fr) auto;
      gap: 10px; align-items: center; font-size: 0.86rem; }
.hb-l { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hb-sub { display: block; font-size: 0.74rem; color: var(--muted-fg); }
.hb-track { height: 10px; border-radius: 5px; background: var(--muted); overflow: hidden; }
.hb-track i { display: block; height: 100%; background: var(--primary);
              border-radius: 2px 5px 5px 2px; }
.hb-v { font-variant-numeric: tabular-nums; font-weight: 600; white-space: nowrap; }
.hb-more { font-size: 0.74rem; color: var(--muted-fg); }
@media (max-width: 560px) {
  .hb { grid-template-columns: 1fr auto; }
  .hb-track { grid-column: 1 / -1; order: 3; }
}

.pipe { display: flex; gap: 2px; height: 10px; border-radius: 5px; overflow: hidden; }
.pipe i { display: block; min-width: 4px; }
.pipe-good { background: var(--success); }
.pipe-warn { background: var(--warning); }
.pipe-mute { background: var(--muted-fg); opacity: .45; }
.pipe-legend { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 7px;
               font-size: 0.8rem; color: var(--muted-fg); }
.pipe-legend b { color: var(--fg); font-variant-numeric: tabular-nums; }
.pipe-dot { display: inline-block; width: 8px; height: 8px; border-radius: 2.5px;
            margin-right: 5px; vertical-align: 0; }

/* ---- rhythm without inline margins ----------------------------------- */
.stack > * + * { margin-top: 14px; }
.stack-sm > * + * { margin-top: 8px; }
.stack-lg > * + * { margin-top: 24px; }

/* ---- accessibility ---------------------------------------------------- */
:focus-visible { outline: 3px solid var(--ring); outline-offset: 1px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}

/* The import screens' column-mapping grid, shared by both importers. */
.im-map { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }

/* ---- event cards on the dashboard ----------------------------------- */
.ev-card { display: flex; flex-direction: column; overflow: hidden; }
.ev-card-media {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 8px; padding: 12px; height: 128px;
  background: linear-gradient(135deg, var(--secondary), var(--primary));
  background-size: cover; background-position: center;
}
.ev-card-body { display: flex; flex-direction: column; gap: 8px; flex: 1 1 auto; }
.ev-card-name { font-size: 1rem; line-height: 1.3; }
.ev-card-actions {
  display: flex; gap: 6px; flex-wrap: wrap; margin-top: auto;
  padding-top: 10px; border-top: 1px solid var(--border);
}

/* ---- the speakers on a public event page ---------------------------- */
.ev-speakers { margin-top: 30px; }
.ev-sp-grid {
  display: grid; gap: 14px;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
}
/* Each speaker is a tile: a surface with a border, the photograph, the
   name and the role, all in one card rather than a portrait floating on
   the page with text loose underneath it. */
.ev-sp {
  margin: 0; text-align: center;
  padding: 18px 12px 16px;
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease;
}
.ev-sp:hover {
  transform: translateY(-3px);
  border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
  box-shadow: var(--shadow-lg);
}

/* A taste of the line-up: eight on a large screen, four on a phone.
   All eight are in the markup and the last four are hidden here, so the
   count follows the width rather than a guess made on the server. */
.ev-sp-more { margin-top: 16px; text-align: center; }

@media (max-width: 640px) {
  .ev-sp-extra { display: none; }
}
/* One shape per card, whatever it holds. The photograph, then the name
   in a box two lines tall, then the role in a box one line tall — so a
   name that wraps cannot push its neighbours' text out of line, which
   is what made a row of speakers read as a scatter. */
.ev-sp-shot { position: relative; display: block; width: 96px; margin: 0 auto 10px; }
.ev-sp img, .ev-sp-initial {
  width: 96px; height: 96px; border-radius: 50%; object-fit: cover;
  display: block; border: 3px solid var(--card);
  box-shadow: var(--shadow-sm);
}
.ev-sp-initial {
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-alt); color: var(--muted-fg);
  font-size: 1.6rem; font-weight: 800;
}

/* The featured mark rides on the photograph. In the text it sat between
   the name and the role, so whether somebody was featured decided where
   everybody else's role line landed. */
.ev-sp-flag {
  position: absolute; left: 50%; bottom: -6px; transform: translateX(-50%);
  font-size: 0.68rem; font-weight: 800; text-transform: uppercase;
  letter-spacing: .08em; white-space: nowrap;
  padding: 2px 9px; border-radius: 99px;
  color: #fff; background: var(--primary);
  box-shadow: 0 0 0 2px var(--card);
}

.ev-sp figcaption { display: block; }
.ev-sp-name {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; font-size: 0.93rem; line-height: 1.3;
  min-height: 2.6em;                     /* two lines, always */
}
.ev-sp-role {
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow: hidden; font-size: 0.8rem; color: var(--muted-fg);
  line-height: 1.4; min-height: 1.4em;   /* reserved even when empty */
  margin-top: 2px;
}

/* =====================================================================
   Life: motion with a purpose, tone where it means something.
   Every animation dies under prefers-reduced-motion (rule above).
   ===================================================================== */

/* Things you can press acknowledge the press. */
.btn { transition: background .15s ease, border-color .15s ease, color .15s ease,
                   transform .1s ease, box-shadow .15s ease; }
.btn:active { transform: scale(.97); }

/* Cards and tiles lift to meet the cursor — only the interactive ones. */
a.stat, .ev-card, .value, .card-link {
  transition: transform .18s ease, box-shadow .18s ease;
}
a.stat:hover, .ev-card:hover, .value:hover, .card-link:hover {
  transform: translateY(-2px); box-shadow: var(--shadow);
}

/* Content arrives, gently. One short rise, not a parade. */
@keyframes rise { from { opacity: 0; transform: translateY(6px); } }
.dash-main > .card, .dash-main > .stats, .dash-main > .alert,
.dash-main > .grid, .dash-main > .table-wrap {
  animation: rise .3s ease both;
}

/* Stat tiles wear their tone on the left edge. */
.stat { border-left: 3px solid var(--border); }
.stat.accent    { border-left-color: var(--primary); }
.stat.attention { border-left-color: var(--warning); border-color: var(--warning); }
.stat.attention .value { color: var(--warning); }
.stat.good      { border-left-color: var(--success); }

/* ---- the meter: one number against its ceiling ----------------------
   A single-hue magnitude mark: 8px tall, 4px rounded ends, the fill
   animating to its width on load. Sold against capacity, used against
   quota — anywhere a number has a maximum worth seeing. */
.meter {
  position: relative; height: 8px; border-radius: 4px;
  background: var(--muted); overflow: hidden;
}
.meter > i {
  position: absolute; top: 0; bottom: 0; left: 0;
  border-radius: 4px; background: var(--primary);
  animation: meter-fill .6s ease both;
}
.meter.full > i { background: var(--success); }
@keyframes meter-fill { from { width: 0 !important; } }
.meter-line {
  display: flex; justify-content: space-between; gap: 8px;
  font-size: 0.74rem; color: var(--muted-fg); margin-top: 4px;
  font-variant-numeric: tabular-nums;
}

/* Admin's two daily tables, labelled by position on the phone —
   markup untouched; the headings live here in the sheet. */
@media (max-width: 700px) {
  .admin-clients td:nth-child(2)::before { content: "Plan"; }
  .admin-clients td:nth-child(3)::before { content: "Tokens"; }
  .admin-clients td:nth-child(4)::before { content: "Events"; }
  .admin-clients td:nth-child(5)::before { content: "Ticketing"; }
  .admin-clients td:nth-child(6)::before { content: "Status"; }
  .admin-events td:nth-child(2)::before  { content: "Organiser"; }
  .admin-events td:nth-child(3)::before  { content: "Date"; }
  .admin-events td:nth-child(4)::before  { content: "Status"; }
  .admin-clients td:nth-child(n+2)::before, .admin-events td:nth-child(n+2)::before {
    display: block; font-size: 0.68rem; text-transform: uppercase;
    letter-spacing: .06em; color: var(--muted-fg);
  }
}

/* ---- global search ---------------------------------------------------- */
.dash-search { padding: 0 14px 14px; }
.dash-search input {
  width: 100%; font-size: 0.86rem; padding: 8px 12px;
  border: 1px solid var(--border); border-radius: 99px;
  background: var(--input-bg); color: var(--fg);
}
.search-hit {
  display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap;
  padding: 9px 8px; margin: 0 -8px; border-radius: 8px;
  color: inherit; text-decoration: none;
  border-bottom: 1px solid var(--border);
}
.search-hit:last-child { border-bottom: 0; }
.search-hit:hover { background: var(--bg-alt); text-decoration: none; }
.search-hit-title { font-weight: 600; }
.search-hit-meta { font-size: 0.8rem; color: var(--muted-fg); }

/* ---- person cards: the membership platform's directory pattern ------ */
.person-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
.person-card {
  display: flex; flex-direction: column; gap: 8px;
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: 14px; background: var(--card);
  transition: transform .15s ease, box-shadow .15s ease;
}
.person-card:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.person-head { display: flex; gap: 11px; align-items: center; min-width: 0; }
.person-avatar, .person-initial {
  width: 48px; height: 48px; border-radius: 50%; flex: 0 0 auto; object-fit: cover;
}
.person-initial {
  display: flex; align-items: center; justify-content: center;
  background: var(--primary); color: #fff; font-weight: 700; font-size: 1.1rem;
}
.person-head b { display: block; font-size: 0.93rem; overflow: hidden;
                 text-overflow: ellipsis; white-space: nowrap; }
.person-head .sub { font-size: 0.8rem; color: var(--muted-fg); overflow: hidden;
                    text-overflow: ellipsis; white-space: nowrap; }
.person-why { font-size: 0.8rem; color: var(--muted-fg); display: flex; gap: 4px; flex-wrap: wrap; }
.person-actions { display: flex; gap: 6px; margin-top: auto; padding-top: 6px; }

/* ---- the listing standard: pager, sort headers, bulk selection ------ */
.lister {
  display: flex; gap: 14px; align-items: center; flex-wrap: wrap;
  padding: 10px 2px; font-size: 0.86rem; color: var(--muted-fg);
}
.lister-count { font-variant-numeric: tabular-nums; }
.lister-per a { color: var(--muted-fg); text-decoration: underline; }
.lister-per strong { color: var(--fg); }
.lister-pages { display: flex; gap: 8px; align-items: center; margin-left: auto;
                font-variant-numeric: tabular-nums; flex-wrap: wrap; }
.lister-pages a { color: var(--primary); }
.lister-here { color: var(--fg); }
th .sort-link { color: inherit; text-decoration: none; white-space: nowrap; }
th .sort-link:hover { text-decoration: underline; }
.bulk-bar {
  position: sticky; bottom: 12px; z-index: 30;
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin-top: 12px; padding: 10px 14px;
  background: var(--card); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
}
.bulk-bar .bulk-count { font-weight: 600; font-size: 0.86rem;
                        font-variant-numeric: tabular-nums; }
.bulk-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.bulk-bar .bulk-clear { margin-left: auto; }
.bulk-row, .bulk-all { width: 15px; height: 15px; accent-color: var(--primary); }

/* ---- value tiles: nine things the platform does --------------------- */
/* They were bare text in a grid — an icon, a bold line, a paragraph, no
   surface — so nine of them read as one wall rather than nine things.
   And nine tiles in a four-column auto-fit grid leaves a lone orphan on
   the last row. Both fixed here: a real card each, and a 3 x 3 grid that
   comes out even. */

.value-band .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 900px) { .value-band .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 580px) { .value-band .grid { grid-template-columns: 1fr; } }

.value {
  position: relative;
  padding: 22px 22px 24px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--card);
  transition: transform .22s cubic-bezier(.2, .7, .3, 1),
              border-color .22s ease, box-shadow .22s ease;
}

/* The accent traces the whole outline instead of sitting as a bar along
   the top: a light travels the perimeter, and the mask fades it out on
   the way down, so the top edge is bright and the bottom is barely
   there. Built from a conic gradient rotated by an animatable custom
   property, masked to a 1.5px ring by the padding-box XOR trick. */
.value::after {
  content: ''; position: absolute; inset: -1px; z-index: 3;
  border-radius: inherit; pointer-events: none;
  opacity: 0; transition: opacity .35s ease;
}
.value:hover {
  transform: translateY(-4px);
  border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
  box-shadow: 0 14px 34px rgba(20, 14, 30, .18);
}
.value:hover::after { opacity: 1; }

.value h3 { font-size: 1.02rem; margin-bottom: 6px; }
.value p  { font-size: 0.93rem; line-height: 1.6; }

.value-icon {
  font-size: 1.4rem;
  width: 44px; height: 44px;
  transition: transform .22s cubic-bezier(.2, .7, .3, 1), background .22s ease;
}
.value:hover .value-icon { transform: translateY(-2px) scale(1.06); }

/* Entrance: the tiles rise in sequence on load. Written as an animation
   with fill-mode both and no opacity:0 in the resting rule — so if the
   animation never runs, for any reason, the tiles are simply there.
   (An earlier pass on this page hid content and leaned on a script to
   reveal it; a backgrounded tab left it invisible. Not again.) */
@media (prefers-reduced-motion: no-preference) {
  .value { animation: valueRise .5s cubic-bezier(.2, .7, .3, 1) backwards; }
  .value:nth-child(1) { animation-delay: .02s; }
  .value:nth-child(2) { animation-delay: .07s; }
  .value:nth-child(3) { animation-delay: .12s; }
  .value:nth-child(4) { animation-delay: .17s; }
  .value:nth-child(5) { animation-delay: .22s; }
  .value:nth-child(6) { animation-delay: .27s; }
  .value:nth-child(7) { animation-delay: .32s; }
  .value:nth-child(8) { animation-delay: .37s; }
  .value:nth-child(9) { animation-delay: .42s; }
}
/* Transform only, never opacity. An animation that is paused — a
   backgrounded tab, a throttled compositor — holds its from-state,
   and a from-state of opacity:0 means invisible content. The worst
   this can do is leave a tile 14px low, still perfectly readable. */
@keyframes valueRise {
  from { transform: translateY(14px); }
  to   { transform: none; }
}

/* ---- hero motion ---------------------------------------------------- */
/* Restrained, and deliberately transform-only. An animation that is
   paused — backgrounded tab, throttled compositor — holds its from-state,
   so a from-state of opacity:0 is a promise to sometimes show nothing.
   Every keyframe below moves things; none of them hide anything. */

@media (prefers-reduced-motion: no-preference) {
  .hero-copy > * { animation: heroRise .62s cubic-bezier(.2, .7, .3, 1) backwards; }
  .hero-copy > :nth-child(1) { animation-delay: .04s; }
  .hero-copy > :nth-child(2) { animation-delay: .12s; }
  .hero-copy > :nth-child(3) { animation-delay: .2s; }
  .hero-copy > :nth-child(4) { animation-delay: .28s; }
  .hero-copy > :nth-child(5) { animation-delay: .36s; }
  .hero-copy > :nth-child(6) { animation-delay: .44s; }

  /* The fan arrives a beat after the words, then keeps breathing. */
  .hero-art { animation: heroArtIn .8s .3s cubic-bezier(.2, .7, .3, 1) backwards; }
  .flyer-stack { animation: flyerFloat 7s ease-in-out 1.4s infinite alternate; }
}

@keyframes heroRise {
  from { transform: translateY(16px); }
  to   { transform: none; }
}
@keyframes heroArtIn {
  from { transform: translateY(22px) scale(.97); }
  to   { transform: none; }
}
@keyframes flyerFloat {
  to { transform: translateY(-12px); }
}

/* The fan already spreads on hover; the whole stack now lifts with it,
   and the search field answers the pointer too. */
.hero-art { transition: transform .3s ease-out; }
.hero-search input[type="search"] { transition: border-color .2s ease, background .2s ease; }
.hero-search input[type="search"]:focus { background: rgba(255,255,255,.08); }
.hero-stats div strong { transition: transform .2s ease; }
.hero-stats div:hover strong { transform: translateY(-2px); }

/* ---- depth on the feature tiles and the hero flyer ------------------ */
/* What is left of a longer experiment. Glass came out muddy over a busy
   page and went; the pointer-following sheen lit the middle of a card
   instead of its edge and went too. What survives is a perspective tilt
   driven by --rx/--ry, which the script sets from the pointer, and the
   travelling stroke further down. CSS owns every visual decision here;
   the script only reports where the pointer is. */

/* Solid, not glass. The frosted treatment was tried here and did not
   earn its place — translucency over a busy page reads as muddy, and
   fifteen backdrop filters cost real frames. The depth now comes from
   the border, the shadow and the tilt, which is enough. */
.value {
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: 0 10px 28px rgba(10, 8, 20, .14);
  transform: perspective(1000px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transform-style: preserve-3d;
  transition: transform .18s ease-out, box-shadow .25s ease, border-color .25s ease;
}
/* No sheen. A bright spot tracking the pointer over a chromatic band was
   tried here and read as a smear of glare sitting in the middle of the
   card — light in the background rather than on the shape. The light on
   these tiles is the stroke that travels the border, and nothing else;
   see "the travelling beam" below. */

.value:hover {
  transform: perspective(1000px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg))
             translateY(-6px) scale(1.012);
  border-color: color-mix(in srgb, var(--primary) 40%, transparent);
  box-shadow: 0 22px 48px rgba(10, 8, 20, .3);
}

/* The icon lifts off its own card, which is what sells the depth. */
.value-icon { transform: translateZ(28px); }
.value h3, .value p { transform: translateZ(12px); }

/* The hero flyer: same grammar, applied to artwork rather than a panel.
   An <img> cannot carry pseudo-elements, so the sheen rides on the
   stack above it. */
.hero-art {
  transform: perspective(1100px) rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg));
  transform-style: preserve-3d;
  transition: transform .2s ease-out;
}
/* The hero flyer had the same sheen the feature tiles did — a bright
   spot following the pointer over a blue-and-pink band. It is gone for
   the same reason: it lit the middle of the artwork instead of its
   edges, and the artwork is the thing worth looking at. The tilt stays;
   that reads as depth rather than glare. */

/* Touch and reduced motion get the flat, honest version: solid panels,
   no tilt, no sheen. Glass without a pointer is just a blurry box. */
@media (hover: none), (prefers-reduced-motion: reduce) {
  .value { transform: none; }
  .value-icon, .value h3, .value p { transform: none; }
  .hero-art { transform: none; }
}

/* ---- the travelling beam ------------------------------------------- */
/* An angle that CSS can actually animate. Declared with @property so the
   browser interpolates it as an angle rather than swapping a string. */
@property --beam {
  syntax: '<angle>';
  initial-value: 0deg;
  inherits: false;
}

/* The ring is a full-size conic gradient with a hole punched through the
   middle: two mask layers, the inner excluded from the outer, leaving
   only the 1.5px edge the padding defines.

   Both layers must be fully opaque. `exclude` is the XOR operator —
   result alpha is a + b - 2ab — so a second layer that fades to .55
   leaves 1 + .55 - 1.1 = .45 alpha across the middle of the card, and
   the conic gradient washes through the interior at nearly half
   strength. That was the coloured glare: not a stray effect, this rule
   quietly failing to cut its own hole. The vertical fade it was there
   for is not worth a leak, so it is gone and this is a plain stroke. */
@supports ((mask-composite: exclude) or (-webkit-mask-composite: xor)) and (background: conic-gradient(from 0deg, red, blue)) {
  .value::after {
    padding: 1.5px;
    background: conic-gradient(from var(--beam),
      transparent 0deg,
      transparent 200deg,
      color-mix(in srgb, var(--primary) 70%, #fff) 268deg,
      #b98bff 302deg,
      color-mix(in srgb, var(--primary) 85%, #fff) 330deg,
      transparent 360deg);

    -webkit-mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask:
      linear-gradient(#000 0 0) content-box,
      linear-gradient(#000 0 0);
    mask-composite: exclude;

    /* Parked until the pointer arrives: fifteen cards spinning a conic
       gradient nobody is looking at is work for nothing. */
    animation: beamTravel 4.5s linear infinite;
    animation-play-state: paused;
  }
  .value:hover::after { animation-play-state: running; }
}

@keyframes beamTravel { to { --beam: 360deg; } }

/* Without @property the angle cannot animate, so the beam would sit
   still. A static ring is a worse answer than the honest one: fall back
   to the lit top edge. */
@supports not (background: conic-gradient(from var(--beam), red, blue)) {
  .value::after {
    inset: -1px -1px auto -1px; height: 2px; border-radius: 14px 14px 0 0;
    background: linear-gradient(90deg, var(--primary), #7c4dbe);
  }
}

@media (hover: none), (prefers-reduced-motion: reduce) {
  .value::after { display: none; }
}

/* ---- events: a hero with light behind it ---------------------------- */
/* The page used to start cold: a heading, four controls, then cards.
   The search is the reason anybody opens this page, so it now sits on a
   lit surface of its own — a field of soft colour behind, the listing's
   own artwork blurred into it, and a frosted panel floating on top that
   carries the heading and every control. Inside the panel everything
   still lines up on one measure, which is what the layout pass fixed. */

.eh {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: 56px 0 50px;
  background: var(--bg);
}

/* The colour field. Three orbs in the brand's own hues rather than the
   reference's purples, sized in viewport units so the composition holds
   its proportions instead of clustering on a wide screen. */
.eh-bg { position: absolute; inset: -10% -5%; z-index: -2; }

.eh-orb {
  position: absolute;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(64px);
  will-change: transform;
}
.eh-orb-1 { width: 30vw; min-width: 300px; background: var(--primary); opacity: .32; top: -14%; right: 5%; }
.eh-orb-2 { width: 26vw; min-width: 260px; background: #6d3fa0;      opacity: .30; bottom: -26%; left: 17%; }
.eh-orb-3 { width: 20vw; min-width: 200px; background: #e08a2e;      opacity: .20; top: 20%; left: -5%; }

/* Real event artwork, blurred past recognition and used as colour. The
   backdrop is made of the listing it introduces, so it changes with the
   filters — and when no event carries its own image the orbs above
   carry the section alone. */
.eh-plate {
  position: absolute;
  width: 22vw; min-width: 200px; aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  filter: blur(40px) saturate(1.45);
  opacity: .38;
}
.eh-plate-1 { top: 2%; right: 27%; }
.eh-plate-2 { bottom: -12%; right: 3%; }
.eh-plate-3 { top: 28%; left: 31%; opacity: .26; }

/* The band melts into the grid below rather than ending on a seam. */
.eh::before {
  content: ''; position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to bottom,
    transparent 0%, transparent 52%,
    color-mix(in srgb, var(--bg) 65%, transparent) 84%,
    var(--bg) 100%);
}

/* The glass. Its own tokens, so it agrees with every other translucent
   surface on the platform and is tuned per theme — the alpha that reads
   as frosted on white reads as dirty on near-black. */
.eh-panel {
  position: relative;
  padding: 30px 34px;
  border-radius: 24px;
  background: var(--glass-bg);
  border: 1px solid var(--glass-line);
  box-shadow: var(--glass-shadow);
  backdrop-filter: blur(var(--glass-blur)) saturate(160%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(160%);
}
/* The lit top edge that makes a pane read as glass rather than as a
   panel someone set the opacity on. */
.eh-panel::before {
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(158deg, rgba(255, 255, 255, .16), transparent 44%);
}
.eh-panel > * { position: relative; }

.eh-eyebrow {
  margin: 0 0 7px;
  font-size: 0.68rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .16em;
  color: var(--primary);
}
/* The brand crimson lands around 2:1 against the dark glass — fine for a
   button fill, far short of the 4.5:1 small text needs. Lifting it
   toward white keeps the brand reading without shouting. */
html[data-theme="dark"] .eh-eyebrow { color: color-mix(in srgb, var(--primary) 45%, #fff); }
.eh-panel h1 { margin: 0 0 4px; }
.events-count { color: var(--muted-fg); margin: 0 0 20px; }

.eh-results { padding-top: 32px; }

/* Full width of the panel, because it is the primary control here and
   the content it filters is full width too. */
.es-search {
  display: flex; gap: 10px; align-items: stretch;
  width: 100%; max-width: none; margin: 0 0 12px;
}
.es-search input[type="search"] {
  flex: 1 1 auto; min-width: 0;
  padding: 15px 20px; font-size: 1rem;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--card) 60%, transparent);
  transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
.es-search input[type="search"]:focus {
  outline: none;
  background: var(--card);
  border-color: color-mix(in srgb, var(--primary) 55%, var(--border));
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 14%, transparent);
}
.es-search .btn { border-radius: 12px; padding-inline: 30px; flex: 0 0 auto; }

/* The refinements sit on the same left edge as the search above them.
   The pill treatment itself now lives on .filters, shared with every
   other screen that narrows a list — see "filter pills" below. `width:
   auto` there is load-bearing: the base form rule sets every select to
   100%, which in a flex row put each one on a line of its own and turned
   four small filters into four full-width bars. */
.es-refine select { border-color: var(--glass-line); }
.es-reset { border-radius: 999px; align-self: center; }

/* A slow drift, so the light behind the glass is alive without asking to
   be watched. Transform-only keyframes: an opacity from-state leaves the
   orbs invisible in a backgrounded tab where the animation never starts. */
@media (prefers-reduced-motion: no-preference) {
  .eh-orb-1 { animation: ehDrift1 19s ease-in-out infinite alternate; }
  .eh-orb-2 { animation: ehDrift2 23s ease-in-out infinite alternate; }
  .eh-orb-3 { animation: ehDrift3 27s ease-in-out infinite alternate; }
}
@keyframes ehDrift1 { to { transform: translate3d(-5%, 6%, 0) scale(1.09); } }
@keyframes ehDrift2 { to { transform: translate3d(7%, -5%, 0) scale(1.06); } }
@keyframes ehDrift3 { to { transform: translate3d(4%, 8%, 0) scale(1.12); } }

@media (max-width: 900px) {
  .eh { padding: 40px 0 38px; }
  .eh-panel { padding: 24px 22px; border-radius: 20px; }
}

/* Two columns on a phone, deliberately. One column turns a listing into
   an endless scroll where nothing can be compared; two lets the eye
   scan. The cards give up their padding and step down a type size to
   earn the room. */
@media (max-width: 620px) {
  .grid-auto { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
  .event-card .card-body, .event-card .body { padding: 12px; }
  .event-card h3 { font-size: 0.93rem; line-height: 1.3; }
  .event-card .muted, .event-card .small { font-size: 0.8rem; }
  .event-card .thumb { aspect-ratio: 16 / 10; }

  /* A 161px card cannot carry what a 272px one carries. Everything a
     phone listing needs to answer is "what, when, how much" — the
     description, the venue line, the category and flyer badges and the
     View button are all reachable one tap away on the event itself, and
     at this width they turn the card into a wall. The whole card is
     already a link, so losing the button costs nothing. */
  .event-card .body > .flex.wrap { display: none; }
  .event-card .body > p.small { display: none; }
  .event-card .meta span:nth-child(2) { display: none; }
  .event-card .foot .btn { display: none; }
  .event-card .foot { padding-top: 2px; }
  .event-card .meta { gap: 2px; font-size: 0.74rem; }
  .eh { padding: 30px 0 30px; }
  .eh-panel { padding: 20px 18px; border-radius: 18px; }
  .eh-results { padding-top: 22px; }
  .es-search { flex-direction: column; }
  .es-search .btn { width: 100%; }
  .es-refine { gap: 7px; }
  /* `min-width: 0` is what makes the 50% basis stick: without it a
     select is at least as wide as its longest option, so the two with
     longer labels each claimed a full row and the four filters came out
     ragged across three. */
  .es-refine select { flex: 1 1 calc(50% - 4px); min-width: 0; }
}
/* ---- the dropdown list ---------------------------------------------- */
/* The list a select drops is drawn by the operating system, so no rule
   in this file could ever reach it — which is why a dark page opened a
   white box with a blue bar in it. assets/js/ui.js draws this one
   instead, as a child of <body> so a card's rounded overflow cannot cut
   it in half. Touch devices keep their native picker on purpose: a
   phone's own sheet beats anything a page can draw. */
.sel-menu {
  position: fixed; z-index: 300;
  max-height: min(320px, 60vh);
  overflow-y: auto;
  /* Reaching the end of a long list must not start scrolling the page
     behind it — that is how a menu appears to jump away mid-choice. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding: 6px;
  border-radius: 14px;
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: 0 18px 44px rgba(10, 8, 20, .28), 0 2px 6px rgba(10, 8, 20, .12);
  /* No fill-mode and no from-state left standing: if the animation never
     ticks — a backgrounded tab, reduced motion — the natural style is
     already the visible one. */
  animation: selIn .13s cubic-bezier(.2, .7, .3, 1);
}
@keyframes selIn { from { opacity: 0; transform: translateY(-5px); } }
.sel-menu.is-up { animation-name: selInUp; }
@keyframes selInUp { from { opacity: 0; transform: translateY(5px); } }

@media (prefers-reduced-motion: reduce) { .sel-menu { animation: none; } }

.sel-opt {
  padding: 9px 12px;
  border-radius: 9px;
  font-size: 0.93rem; line-height: 1.3;
  color: var(--fg);
  cursor: pointer;
  /* The list is as wide as it needs to be, and a long option wraps
     rather than being cut off — a truncated choice is not a choice. */
  overflow-wrap: anywhere;
}
.sel-opt.is-active { background: var(--muted); }
.sel-opt.is-selected { font-weight: 700; color: var(--primary); }
html[data-theme="dark"] .sel-opt.is-selected { color: color-mix(in srgb, var(--primary) 45%, #fff); }
.sel-opt.is-selected.is-active { background: color-mix(in srgb, var(--primary) 12%, var(--muted)); }
.sel-opt.is-disabled { opacity: .45; cursor: default; }

.sel-group {
  padding: 9px 12px 5px;
  font-size: 0.68rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .1em;
  color: var(--muted-fg);
}

/* An open select should look open, the way the native one does. */
select[aria-expanded="true"] {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--ring);
}

/* ---- filter pills ---------------------------------------------------- */
/* The rounded filter treatment the events hero introduced, available to
   any screen that filters a list: put .filters on the row. Kept separate
   from the base form styling, because a pill is right for "narrow this
   list" and wrong for "fill this in". */
.filters {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
}
.filters select,
.filters input[type="search"],
.filters input[type="text"],
.filters input[type="date"] {
  width: auto; flex: 0 0 auto;
  padding: 9px 14px; font-size: 0.86rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background-color: color-mix(in srgb, var(--card) 55%, transparent);
  cursor: pointer;
  transition: border-color .18s ease, background-color .18s ease;
}
.filters select { padding-right: 32px; background-position: right 12px center; }
.filters select:hover,
.filters input:hover {
  border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
  background-color: color-mix(in srgb, var(--card) 85%, transparent);
}
.filters input[type="search"], .filters input[type="text"], .filters input[type="date"] {
  cursor: text;
}
.filters .btn { border-radius: 999px; }

/* The toolbar is the same idea under an older name — every dashboard and
   admin screen that narrows a list uses it — so it gets the same
   treatment rather than a second look that means the same thing. Its
   labels stay: a toolbar's controls are not always self-describing the
   way "All categories" is, and a bare pill with no caption is a guess.
   They shrink to captions above the control instead of disappearing. */
.toolbar .field label {
  font-size: 0.68rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted-fg);
  margin-bottom: 5px; padding-left: 4px;
}
.toolbar .field input[type="search"],
.toolbar .field input[type="text"],
.toolbar .field select {
  border-radius: 999px;
  padding-left: 16px;
  transition: border-color .18s ease, box-shadow .18s ease;
}
.toolbar .field select { padding-right: 34px; background-position: right 13px center; cursor: pointer; }
.toolbar .field select:hover {
  border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
}
.toolbar .btn { border-radius: 999px; }

/* While live results are on their way, the old ones go quiet rather than
   disappearing — a list that empties and refills reads as a page break. */
[data-live][aria-busy="true"] { opacity: .55; transition: opacity .15s ease; }

/* ---- pricing: the card ----------------------------------------------- */
/* A column, so the button sits at the bottom whatever the feature list
   above it does. This was an inline style on the element, where no
   template could override it — which is why the row layout below could
   set grid-template-columns all it liked and still get a flex column. */
.price-card > .card-body {
  display: flex; flex-direction: column; height: 100%;
}

/* ---- pricing: four across -------------------------------------------- */
/* Four plans in four columns is tighter than three, so the card gives up
   padding and steps its price down rather than letting the columns go
   ragged. It still folds to two and then one on the way down, which
   grid-4 already handles. */
.price-four .price-card > .card-body { padding: 20px 18px; }
.price-four .price-card .amount { font-size: 1.5rem; }
.price-four .price-card ul { font-size: 0.86rem; }

/* ---- pricing: the scroller ------------------------------------------- */
/* For a catalogue that will not fit a screen at any column count. One
   row, swiped or scrolled sideways, each card snapping into place. It is
   focusable and labelled because a scrolling region a keyboard cannot
   reach is a region some people simply do not get. */
.price-scroll {
  display: flex; gap: 16px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Room for the raised card's lift and its shadow, which a plain
     overflow container would otherwise clip. */
  padding: 6px 4px 16px;
  scrollbar-width: thin;
}
.price-scroll .price-card {
  flex: 0 0 clamp(240px, 78vw, 300px);
  scroll-snap-align: start;
}
.price-scroll:focus-visible { outline: 2px solid var(--primary); outline-offset: 4px; border-radius: var(--radius); }

/* ---- pricing: one row per plan --------------------------------------- */
/* Three columns stop working once a catalogue outgrows them — the cards
   go narrow, the feature lists go ragged, and the prices stop lining up
   with each other, which is the one thing a pricing page has to do. This
   is the same cards, laid out one per row: the price and the button sit
   in a fixed column on the right so they line up down the page, and the
   plan's own copy takes whatever width is left. Chosen per site in
   /admin/packages. */
.price-rows { display: flex; flex-direction: column; gap: 12px; }
.price-rows .price-card { width: 100%; }

/* Explicit placement, not named areas. Named areas only work when every
   child is assigned one; the copy here was placed by column instead, so
   it auto-flowed into the grid and shoved the price out of the column it
   was supposed to line up in. Row and column on each child is longer to
   write and cannot drift. */
.price-rows .price-card > .card-body {
  display: grid;   /* beats the flex column above — same element, more specific */
  grid-template-columns: minmax(0, 1fr) 220px;
  column-gap: 28px; row-gap: 6px;
  align-items: start;
  height: auto;
}
.price-rows .price-card > .card-body > *          { grid-column: 1; }
.price-rows .price-card > .card-body > h3         { grid-row: 1; }
.price-rows .price-card > .card-body > .price-tag { grid-row: 2; }
.price-rows .price-card > .card-body > ul         { grid-row: 3; columns: 2; column-gap: 26px; margin-top: 10px; }
/* The price and its button share the right-hand column, which is what
   makes the prices line up down the page — the one thing a list of
   plans has to get right. */
.price-rows .price-card > .card-body > .amount {
  grid-column: 2; grid-row: 1 / span 2;
  text-align: right; margin: 0;
}
.price-rows .price-card > .card-body > .btn { grid-column: 2; grid-row: 3; }

@media (max-width: 720px) {
  /* A 220px price column on a phone leaves nothing for the plan. */
  .price-rows .price-card > .card-body { grid-template-columns: 1fr; }
  .price-rows .price-card > .card-body > * { grid-column: 1; grid-row: auto; }
  .price-rows .price-card > .card-body > .amount { text-align: left; }
  .price-rows .price-card > .card-body > ul { columns: 1; }
}

/* ---- report tables and their cut tabs -------------------------------- */
/* Shared by the session attendance report and the accreditation gate log,
   which ask the same question of different doors. These began inline on
   one page; the second page needing them is the moment they stop being
   that page's business. */
.att-tabs {
  display: flex; gap: 2px; flex-wrap: wrap;
  background: var(--bg-alt); padding: 3px;
  border-radius: 10px; border: 1px solid var(--border);
}
.att-tabs a {
  padding: 7px 14px; border-radius: 7px;
  font-size: 0.86rem; font-weight: 600;
  color: var(--muted-fg); text-decoration: none; white-space: nowrap;
}
.att-tabs a:hover { color: var(--fg); }
.att-tabs a[aria-current] { background: var(--card); color: var(--fg); box-shadow: var(--shadow-sm); }

table.att { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
table.att th {
  text-align: left; font-size: 0.74rem; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted-fg);
  padding: 10px 12px; border-bottom: 1px solid var(--border); white-space: nowrap;
}
table.att td { padding: 10px 12px; border-bottom: 1px solid var(--border); vertical-align: top; }
table.att tr:last-child td { border-bottom: 0; }
/* Figures line up in a column or they cannot be compared down it. */
table.att td.t, table.att th.t {
  font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right;
}
table.att tfoot th { border-top: 1px solid var(--border); border-bottom: 0; color: var(--fg); }
table.att td.who b { display: block; }
table.att td.who span { font-size: 0.8rem; color: var(--muted-fg); }
.att-wrap { overflow-x: auto; }

/* ---- back to top ----------------------------------------------------- */
/* Every long page gets one. It stays out of the way until there is
   something to come back from, and it is a real link to #top so it works
   with scripting off. */
.to-top {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  width: 42px; height: 42px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--card); color: var(--fg);
  border: 1px solid var(--border);
  box-shadow: 0 8px 22px rgba(10, 8, 20, .22);
  opacity: 0; visibility: hidden; transform: translateY(10px);
  transition: opacity .25s ease, transform .25s ease, visibility .25s;
}
.to-top.on { opacity: 1; visibility: visible; transform: none; }
.to-top:hover {
  border-color: color-mix(in srgb, var(--primary) 45%, var(--border));
  transform: translateY(-2px);
}
.to-top svg { width: 18px; height: 18px; stroke: currentColor; stroke-width: 2;
              fill: none; stroke-linecap: round; stroke-linejoin: round; }

@media (prefers-reduced-motion: reduce) { .to-top { transition: none; } }

/* =====================================================================
   Sheet — the editing popup
   ---------------------------------------------------------------------
   Editing used to mean following a link to ?edit=<id>, which reloaded
   the page and dropped an open <details> somewhere below the record you
   came from. On a programme with a hundred and eighteen sessions that is
   a scroll each way, and you lose your place in the list every time.

   Now the same form opens over the list. Built on <dialog> so the focus
   trap, the Escape key and the inert background are the browser's job
   rather than ours — the hand-rolled versions of those are where modals
   usually go wrong for anyone using a keyboard.

   Without scripting the link still navigates and the form still renders
   in the page. This is an upgrade to a working thing, not a replacement.
   ===================================================================== */

.sheet {
  padding: 0;
  border: 0;
  width: min(760px, calc(100vw - 2rem));
  max-height: min(88vh, 900px);
  border-radius: var(--radius-lg);
  background: var(--card);
  color: var(--card-fg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

.sheet::backdrop {
  background: rgba(16, 20, 24, .55);
  backdrop-filter: blur(3px);
}

/* Opens from slightly below and small, which reads as the panel coming
   forward rather than blinking into existence. */
@keyframes sheet-in {
  from { opacity: 0; transform: translateY(12px) scale(.985); }
  to   { opacity: 1; transform: none; }
}

.sheet[open] { animation: sheet-in .18s cubic-bezier(.2, .8, .3, 1); }
.sheet[open]::backdrop { animation: sheet-fade .18s ease; }

@keyframes sheet-fade { from { opacity: 0; } to { opacity: 1; } }

.sheet-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  /* Stays put while the form scrolls: on a long form the title is the
     one thing that says what you are editing. */
  position: sticky;
  top: 0;
  background: var(--card);
  z-index: 2;
}

.sheet-head h3 { margin: 0; font-size: 1.02rem; font-weight: 700; flex: 1 1 auto; min-width: 0; }
.sheet-head .note { display: block; font-size: 0.8rem; font-weight: 400; color: var(--muted-fg); }

.sheet-close {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--bg-alt);
  color: var(--muted-fg);
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  transition: background .15s ease, color .15s ease, border-color .15s ease;
}

.sheet-close:hover { background: var(--primary-50); color: var(--primary); border-color: var(--primary); }

.sheet-body {
  padding: 18px 20px 22px;
  overflow-y: auto;
  max-height: calc(min(88vh, 900px) - 64px);
  overscroll-behavior: contain;
}

/* The form arrives from a page where it was a <details> fold. Inside the
   sheet the fold is redundant — the sheet is the disclosure — so its
   chrome is dropped rather than the markup being duplicated per page. */
.sheet-body > .fold,
.sheet-body > .card { border: 0; padding: 0; margin: 0; background: none; box-shadow: none; }
.sheet-body > .fold > summary { display: none; }

.sheet-busy {
  display: grid;
  place-items: center;
  min-height: 160px;
  color: var(--muted-fg);
  font-size: 0.93rem;
}

/* On a phone it is a bottom sheet: a centred dialog on a small screen
   puts the fields under the thumb and the keyboard over them. */
@media (max-width: 640px) {
  .sheet {
    width: 100vw;
    max-width: 100vw;
    max-height: 92vh;
    margin: auto 0 0;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }

  .sheet-body { max-height: calc(92vh - 64px); }

  @keyframes sheet-in {
    from { opacity: 0; transform: translateY(100%); }
    to   { opacity: 1; transform: none; }
  }
}

@media (prefers-reduced-motion: reduce) {
  .sheet[open], .sheet[open]::backdrop { animation: none; }
}

/* ---- touch targets the label strategy does not reach ---------------
   The mobile block further down already handles this properly: buttons
   gain height, and a checkbox stays its intrinsic size because the
   label.check wrapping it carries a 44px target. That is a better
   answer than fattening every box, and it is left alone.

   Two things sit outside it. A row-select checkbox in a table has no
   label to hide behind — it is bare in a <td>, and it measured 15px on
   a phone, on the screens where somebody ticks forty of them. And the
   small chrome — remove, close, sign out, back — measured 16 to 27px.

   24px is the WCAG 2.5.8 minimum and is deliberate for the table: a
   44px box in a list of five hundred attendees would push the row it
   belongs to off the screen. */
@media (max-width: 640px) {
  .bulk-row, .bulk-all, .cf-pick, .sp-on { width: 24px; height: 24px; }

  .x, .signout, .crumb-back, .att-remove,
  .tchip .x, .chip .x { min-height: 44px; min-width: 44px;
                        display: inline-flex; align-items: center; justify-content: center; }
}

/* ---- quick actions ---------------------------------------------------
   A bar of the jobs an organiser repeats all week — printing above all —
   pinned where a thumb already is.

   Four things already live at the bottom of a dashboard screen: the bulk
   bar on every listing, the save bar on every form, back-to-top, and the
   toast stack. All four are told how tall this is and move up by it, so
   the bar never covers a Save button or the actions for rows somebody
   just ticked. That is the whole reason --qa-h exists rather than a
   hardcoded offset in five places. */
:root { --qa-h: 0px; }
/* Measured, per breakpoint: on a desktop the bar is the same height
   open or closed, because closing only hides the strip beside a
   toggle that was already the tallest thing in it. */
body:has(.qa) { --qa-h: 51px; padding-bottom: calc(var(--qa-h) + 14px); }

.qa {
  position: fixed; z-index: 50;
  left: 50%; transform: translateX(-50%);
  bottom: 12px; max-width: min(1100px, calc(100vw - 24px));
  display: flex; align-items: center; gap: 8px;
  padding: 6px; border-radius: 999px;
  background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur));
  border: 1px solid var(--glass-line); box-shadow: var(--glass-shadow);
}

.qa-toggle {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  border: 0; border-radius: 999px; cursor: pointer;
  padding: 8px 14px; font: inherit; font-size: var(--t-sm); font-weight: 700;
  background: var(--primary); color: #fff;
}
.qa-toggle-tx { white-space: nowrap; }
.qa-chev { transition: transform .15s ease; }
.qa[data-open="1"] .qa-chev { transform: rotate(90deg); }

/* The list scrolls inside the bar rather than pushing the page wide —
   an event with every module on has more of these than a phone is.
   Hidden rather than removed when closed, so the toggle keeps its
   place and nothing reflows. */
.qa-list {
  display: flex; align-items: center; gap: 4px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  /* No scrollbar, for a reason beyond looks: a horizontal bar
     claims ~10px of height only when the strip overflows, so the
     bar was 68px tall on a screen with eight actions and 58px on
     one with two — and --qa-h, which everything else positions
     against, cannot be two numbers. The fade says there is more.
     Scrolling still works by touch, wheel and keyboard. */
  scrollbar-width: none;
  mask-image: linear-gradient(to right, #000 calc(100% - 22px), transparent);
}
.qa-list::-webkit-scrollbar { display: none; }
.qa[data-open="0"] .qa-list { display: none; }

.qa-item {
  display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto;
  padding: 8px 13px; border-radius: 999px;
  font-size: var(--t-sm); font-weight: 600; white-space: nowrap;
  color: var(--fg); text-decoration: none;
}
.qa-item:hover { background: var(--muted); text-decoration: none; }
.qa-item .ni { color: var(--muted-fg); flex: none; }
.qa-item:hover .ni { color: var(--primary-fg); }

/* The event chip, shown only when the bar guessed which event these act
   on. Reads as a label rather than another action. */
.qa-ev { font-weight: 700; }
.qa-ev span { max-width: 15ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qa-ev .ni { color: var(--ok); }
.qa-ev + .qa-item { border-left: 1px solid var(--border); margin-left: 2px; padding-left: 12px; }

/* Out of the way of the things that were here first. */
body:has(.qa) .bulk-bar      { bottom: calc(12px + var(--qa-h)); }
body:has(.qa) .sticky-actions { bottom: var(--qa-h); }
body:has(.qa) .to-top        { bottom: calc(20px + var(--qa-h)); }
body:has(.qa) .toasts        { bottom: calc(18px + var(--qa-h)); }

@media (max-width: 640px) {
  /* The toggle grows to a 44px target here, so the bar is taller than
     it is on a desktop and everything told to sit above it has to know
     the new number. Measured, not guessed: 68 open, 58 closed. */
  body:has(.qa) { --qa-h: 58px; }

  .qa { left: 12px; right: 12px; transform: none; max-width: none; }
  .qa-toggle-tx { display: none; }          /* the icon carries it */
  .qa-toggle { min-height: 44px; min-width: 44px; justify-content: center; }
  .qa-item { min-height: 44px; }
}

@media print { .qa { display: none; } }

/* ---- the first-run tour --------------------------------------------
   Four steps around the furniture, once. The ring is drawn around the
   real element rather than over a screenshot, so it cannot go stale
   the way a picture of a sidebar does. */
.tour { position: fixed; inset: 0; z-index: 200; }
.tour-veil { position: absolute; inset: 0; background: rgba(8, 8, 12, .55); }

.tour-ring {
  position: fixed; border-radius: 12px; pointer-events: none;
  border: 2px solid var(--primary);
  box-shadow: 0 0 0 9999px rgba(8, 8, 12, .55);
  transition: top .18s ease, left .18s ease, width .18s ease, height .18s ease;
}
/* With a ring on screen its own shadow is the veil, so the flat one
   underneath would double the dimming. */
.tour:has(.tour-ring:not([hidden])) .tour-veil { opacity: 0; }

.tour-card {
  position: fixed; width: min(340px, calc(100vw - 24px));
  background: var(--card); color: var(--card-fg);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--lift-3); padding: 16px 18px;
  transition: top .18s ease, left .18s ease;
}
.tour-card:focus { outline: none; }
.tour-count { font-size: var(--t-2xs); text-transform: uppercase; letter-spacing: .07em;
              color: var(--muted-fg); font-weight: 700; margin: 0 0 6px; }
.tour-card h3 { font-size: var(--t-lg); margin: 0 0 6px; }
.tour-body { font-size: var(--t-md); color: var(--muted-fg); line-height: 1.55; margin: 0 0 14px; }
.tour-btns { display: flex; align-items: center; gap: 8px; }
.tour-btns .grow { flex: 1 1 auto; }

@media (prefers-reduced-motion: reduce) {
  .tour-ring, .tour-card { transition: none; }
}

/* ---- the filter bar stays reachable ---------------------------------- */
/*
 * A search you have to scroll back up to reach is a search people stop
 * using. On four hundred attendees the box is off screen by the second
 * row anyone cares about, and the way out is the browser's own find —
 * which searches the page rather than the data, and so lies about how
 * many matches there are.
 *
 * Sticky under the header rather than fixed: the header is itself
 * sticky at top:0 and exactly --header-h tall, so the two sit flush
 * with no gap for rows to show through. z-index sits below the header
 * (50) and above ordinary content.
 *
 * Padding rather than margin does the spacing, because the background
 * has to cover the full band — a margin leaves a transparent strip that
 * table rows scroll through.
 *
 * Every .toolbar on the platform is a direct child of the page column
 * and so sits on --bg. That was checked, all seven of them. If one is
 * ever put inside a card it will want --card instead, and it will look
 * obviously wrong straight away, which is the right kind of failure.
 */
.toolbar {
  position: sticky;
  top: var(--header-h);
  z-index: 20;
  background: var(--bg);
  padding-block: 12px;
  margin-bottom: 4px;
}

/*
 * Only once it has actually stuck. Applied at rest it reads as a second
 * header permanently parked under the first, which is heavier than the
 * page needs when nothing is scrolling. ui.js sets the class.
 */
.toolbar.is-stuck {
  border-bottom: 1px solid var(--border);
  box-shadow: 0 10px 16px -14px rgb(0 0 0 / 55%);
}

/* Printing a pinned bar over the first rows helps nobody. */
@media print {
  .toolbar { position: static; box-shadow: none; border-bottom: 0; }
}

/* ---- the public events search, pinned ---------------------------------
 *
 * Its own band below the hero rather than inside the glass panel, and
 * that is a requirement rather than a preference: sticky is bounded by
 * its parent's box, the panel ends before the results start, and the
 * panel's backdrop-filter makes it the containing block for fixed
 * children too — so neither mechanism works from in there.
 *
 * Only the bar pins. The eyebrow, the title and the count scroll away,
 * which is the point: what you need at row two hundred is the search,
 * not the word "Events".
 */
.events-dock {
  position: sticky;
  top: var(--header-h);
  z-index: 30;
  background: var(--bg);
  padding-block: 14px;
}

.events-dock .events-form { margin: 0; }

.events-dock.is-stuck {
  border-bottom: 1px solid var(--border);
  box-shadow: 0 10px 16px -14px rgb(0 0 0 / 55%);
}

/* Pinned over the first cards helps nobody on paper. */
@media print {
  .events-dock { position: static; box-shadow: none; border-bottom: 0; }
}

/* ---- a pill-shaped link ------------------------------------------------
 *
 * The guide grew its own copy of this and so did the landing page. One
 * component, in the design system, so the third place that needs it
 * inherits the focus ring and the touch target rather than reinventing
 * them at 5px of padding.
 */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 5px 14px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: var(--t-sm);
  font-weight: 600;
  color: var(--muted-fg);
  text-decoration: none;
  white-space: nowrap;
}

.pill:hover { border-color: var(--primary); color: var(--primary-fg); text-decoration: none; }

/* The chosen one reads as chosen without colour alone carrying it — the
   border weight and the ink both change. */
.pill.on {
  border-color: var(--primary);
  color: var(--primary-fg);
  background: var(--primary-tint, transparent);
  font-weight: 700;
}

/* ---- an organiser's own landing page -----------------------------------
 *
 * Their name is the masthead. Evenz appears once, small, at the bottom,
 * because whoever is buying a ticket is entitled to know who is handling
 * the payment — but this is the organiser's page, not ours.
 *
 * The accent is a FILL and only a fill. It fills the eyebrow and the
 * buttons, and --lp-on-accent is the ink that survives on it, worked out
 * from luminance rather than hoped for. It is never text on the page
 * background: an organiser picks from a swatch, and the first navy
 * chosen here put the title at roughly 1.3:1 against a dark page.
 */
.lp {
  --lp-ink: var(--lp-accent, var(--primary));
  --lp-on: var(--lp-on-accent, #fff);
}

/* The accent as a fill, everywhere it appears. */
.lp .pill.on,
.lp .btn-primary {
  background: var(--lp-ink);
  border-color: var(--lp-ink);
  color: var(--lp-on);
}

/* ---- the masthead ------------------------------------------------------
 *
 * A surface rather than a gap. The wash is the organiser's colour at a
 * few per cent — enough to tint the block and seat it against the page,
 * nowhere near enough to fight the type sitting on it. color-mix keeps
 * that honest whatever colour arrives.
 */
.lp-head {
  position: relative;
  padding: var(--space-2xl) 0 var(--space-xl);
  background:
    radial-gradient(120% 140% at 8% 0%,
      color-mix(in srgb, var(--lp-ink) 14%, transparent) 0%,
      transparent 62%),
    linear-gradient(180deg,
      color-mix(in srgb, var(--lp-ink) 6%, transparent) 0%,
      transparent 100%);
  border-bottom: 1px solid var(--border);
}

.lp-logo {
  display: block;
  max-height: 72px;
  max-width: 240px;
  width: auto;
  margin-bottom: var(--space-md);
  object-fit: contain;
}

/* The count, wearing the accent. Small, upper, tracked — a label rather
   than a sentence, so the name below it is unmistakably the headline. */
.lp-eyebrow {
  display: inline-block;
  margin: 0 0 var(--space-sm);
  padding: 5px 12px;
  border-radius: 999px;
  background: var(--lp-ink);
  color: var(--lp-on);
  font-size: var(--t-2xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.lp-title {
  margin: 0;
  /* Ordinary page ink. The accent is behind it, not in it. */
  color: var(--fg);
  font-size: clamp(2.1rem, 1.1rem + 3.4vw, 3.4rem);
  line-height: 1.04;
  letter-spacing: -0.028em;
  font-weight: 800;
  text-wrap: balance;
}

.lp-intro {
  margin: var(--space-sm) 0 0;
  max-width: var(--measure, 62ch);
  color: var(--muted-fg);
  font-size: 1.06rem;
  line-height: 1.6;
  text-wrap: pretty;
}

/* The one piece of us. Seated on a hairline so it reads as a colophon
   rather than an orphaned sentence. */
.lp-by {
  margin: var(--space-xl) 0 var(--space-2xl);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border);
  text-align: center;
  font-size: var(--t-sm);
  color: var(--muted-fg);
}

.lp-by a { color: var(--muted-strong); }

@media (max-width: 640px) {
  .lp-head { padding: var(--space-xl) 0 var(--space-lg); }
  .lp-logo { max-height: 52px; }
}

/* ---- landing page templates --------------------------------------------
 *
 * Four layouts, one markup. The template is a class on the wrapper and
 * everything below hangs off it, so a new one is a class and a rule
 * rather than a second view to keep in step with the first.
 *
 * --lp-accent is set inline by the view, from a value validated as six
 * hex digits before it goes anywhere near a style attribute. Unset, the
 * page falls back to the platform's own accent and looks exactly as it
 * did before any of this existed.
 */
/* Links pick up the accent on hover only — a whole page of accent-tinted
   links is a page with no emphasis left to give. */
.lp a:not(.btn):not(.pill):hover { color: var(--lp-ink); }

/* ---- Poster: a picture across the top, the name over it --------------- */
.lp-head.has-cover { position: relative; padding: 0; }

.lp-head.has-cover .lp-cover {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.lp-head.has-cover .lp-cover img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}

/* A scrim, because a photograph is not a background until the type on it
   can be read. Dark enough at the bottom for white text at any image. */
.lp-head.has-cover .lp-cover::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgb(0 0 0 / 25%) 0%, rgb(0 0 0 / 78%) 100%);
}

.lp-head.has-cover > .container {
  position: relative;
  padding-top: 130px;
  padding-bottom: 34px;
}

.lp-head.has-cover .lp-title,
.lp-head.has-cover .lp-intro,
.lp-head.has-cover .lp-count { color: #fff; }

.lp-head.has-cover .lp-intro,
.lp-head.has-cover .lp-count { opacity: 0.92; }

/* ---- Programme: a row per event rather than a card ------------------- */
.lp-rows { display: flex; flex-direction: column; gap: 10px; }

/* The same card, laid on its side. Reusing it rather than writing a row
   component keeps one card to maintain — the alternative is two, and the
   second one is always the one that misses a badge. */
.lp-rows .event-card {
  display: grid;
  grid-template-columns: 148px minmax(0, 1fr);
  align-items: stretch;
}

.lp-rows .event-card img { height: 100%; min-height: 104px; object-fit: cover; }

@media (max-width: 640px) {
  .lp-rows .event-card { grid-template-columns: 1fr; }
  .lp-rows .event-card img { min-height: 150px; }
}

/* ---- Next up: the nearest event, larger ------------------------------- */
.lp-lead { margin-bottom: 22px; }

.lp-lead .event-card {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  align-items: stretch;
}

.lp-lead .event-card img { height: 100%; min-height: 240px; object-fit: cover; }

@media (max-width: 760px) {
  .lp-lead .event-card { grid-template-columns: 1fr; }
  .lp-lead .event-card img { min-height: 180px; }
}

@media (max-width: 640px) {
  .lp-head.has-cover > .container { padding-top: 92px; padding-bottom: 26px; }
}


/* ---------------------------------------------------------------------
   A blog article, and the space beside it
   ---------------------------------------------------------------------
   .prose is capped at 72ch because that is the measure prose is read
   at, and widening it to fill a 1400px screen would make the article
   harder to read, not easier. The complaint was never really about the
   measure: it was that the column sat against the left edge with half
   the page empty beside it.

   So the space gets used. On a wide screen the article and a rail of
   its own metadata sit side by side; below that the rail drops under
   the article and everything is one column. */
.post-layout {
    display: grid;
    gap: 40px;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
}

.post-body { min-width: 0; }

.post-rail-inner {
    border-left: 3px solid var(--border);
    padding-left: 18px;
}

.post-rail p { margin-bottom: 4px; }
.post-rail p + .small.strong { margin-top: 16px; }

/* The standfirst, when somebody wrote one of their own. */
.post-stand {
    font-size: 1.12rem;
    line-height: 1.6;
    color: var(--muted-fg);
    border-left: 3px solid var(--primary);
    padding-left: 16px;
    margin-bottom: 28px;
    max-width: 72ch;
}

@media (min-width: 1000px) {
    .post-layout { grid-template-columns: minmax(0, 72ch) minmax(200px, 1fr); }

    /* Sticky so it stays with the reader down a long article, and
       bounded so a tall rail can still scroll on a short screen. */
    .post-rail-inner {
        position: sticky;
        top: 96px;
        max-height: calc(100vh - 120px);
        overflow-y: auto;
    }
}


/* ---------------------------------------------------------------------
   The blog editor
   --------------------------------------------------------------------- */
.editor-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    padding: 8px;
    border: 1px solid var(--border);
    border-bottom: 0;
    border-radius: var(--radius) var(--radius) 0 0;
    background: var(--bg-alt);
}

.editor-sep {
    width: 1px;
    align-self: stretch;
    background: var(--border);
    margin: 2px 4px;
}

/* Not monospace. The old field was, which quietly told everybody they
   were writing code rather than prose — and then collapsed their
   paragraphs when they wrote prose anyway. */
.editor-area {
    border-radius: 0 0 var(--radius) var(--radius);
    font-size: 1rem;
    line-height: 1.7;
    min-height: 420px;
}

.editor-preview {
    border: 1px solid var(--border);
    border-top: 0;
    border-radius: 0 0 var(--radius) var(--radius);
    padding: 20px 22px;
    min-height: 420px;
    max-width: none;
}

/* ---- the attendee's own links --------------------------------------
   One ticket opens several things, and until now each of them lived at
   an address somebody had to have been sent. This is the map: a card
   per destination, big enough to hit with a thumb, with a line saying
   what is behind it — because "Documents" tells nobody anything.       */
.plinks {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  margin: 0 0 18px;
}
.plink {
  display: flex; gap: 10px; align-items: flex-start;
  border: 1px solid var(--border); border-radius: 12px;
  padding: 12px 14px; background: var(--card);
  color: inherit; text-decoration: none;
  transition: border-color .12s ease, transform .12s ease;
}
.plink:hover {
  border-color: var(--primary); text-decoration: none; transform: translateY(-1px);
}
.plink:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; }
.plink .ic { flex: 0 0 auto; margin-top: 2px; color: var(--primary); }
.plink b { display: block; font-size: 0.94rem; line-height: 1.25; }
.plink span { display: block; font-size: 0.76rem; color: var(--muted-fg); margin-top: 2px; }

@media (max-width: 480px) {
  .plinks { grid-template-columns: 1fr; }
}

/* ---- the attendee's pass ---------------------------------------------
   The identity card was a bordered box with a name in it, which is what
   a form field looks like. This is the one place on the page that gets
   to be bold: their own face, their name, the class of ticket, and the
   code — the things a printed pass would carry, in that order.         */
.ppass {
  background:
    radial-gradient(120% 140% at 100% 0%, color-mix(in srgb, var(--primary) 14%, transparent), transparent 60%),
    var(--card);
  border-color: color-mix(in srgb, var(--primary) 26%, var(--border));
}
.ppass-top { display: flex; gap: 14px; align-items: center; }
.ppass-face {
  flex: 0 0 auto; width: 56px; height: 56px; border-radius: 50%;
  object-fit: cover; background: var(--muted);
  border: 2px solid color-mix(in srgb, var(--primary) 35%, var(--border));
}
.ppass-initial {
  display: grid; place-items: center;
  font-size: 1.35rem; font-weight: 700; color: var(--primary);
}
.ppass .who { font-size: 1.18rem; line-height: 1.2; }
.ppass-kind {
  display: inline-block; margin-top: 6px; padding: 2px 10px;
  border-radius: 999px; font-size: 0.72rem; font-weight: 600;
  letter-spacing: .03em; text-transform: uppercase;
  background: color-mix(in srgb, var(--primary) 15%, transparent);
  color: var(--primary);
}
.ppass-code {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--border);
}
.ppass-code span { font-size: 0.72rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted-fg); }
.ppass-code code {
  font-size: 1.05rem; font-weight: 600; letter-spacing: .12em;
  user-select: all; font-variant-numeric: tabular-nums;
}

/* ---- a link that is not ready yet ------------------------------------
   Deliberately quiet and deliberately present. Greyed so it does not
   compete, kept so nobody concludes their certificate was lost.        */
.plink.is-waiting {
  background: var(--muted); border-style: dashed; cursor: default;
}
.plink.is-waiting:hover { border-color: var(--border); transform: none; }
.plink.is-waiting .ic { color: var(--muted-fg); }
.plink.is-waiting b { color: var(--muted-fg); font-weight: 600; }
.plink.is-waiting b em {
  font-style: normal; font-size: 0.68rem; text-transform: uppercase;
  letter-spacing: .05em; margin-left: 6px; padding: 1px 7px;
  border-radius: 999px; background: var(--border); color: var(--muted-fg);
}

/* ---- fifty-six sessions, read on a phone -----------------------------
   At this length the day heading scrolls away in the first flick and
   every card after it looks the same as the last. The heading sticks,
   and each slot grows a rail so the eye has a spine to run down.       */
.pday > h2 {
  position: sticky; top: 0; z-index: 2;
  background: var(--bg); padding-top: 10px;
}
.pslot { position: relative; padding-left: 16px; }
.pslot::before {
  content: ""; position: absolute; left: 3px; top: 6px; bottom: 14px;
  width: 2px; border-radius: 2px;
  background: linear-gradient(var(--border), transparent);
}
.pslot > h3 { position: relative; }
.pslot > h3::before {
  content: ""; position: absolute; left: -16px; top: 4px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--primary); box-shadow: 0 0 0 3px var(--bg);
}
.psess { transition: border-color .12s ease, box-shadow .12s ease; }
.psess:hover { border-color: color-mix(in srgb, var(--primary) 40%, var(--border)); }

@media (max-width: 560px) {
  .ppass-face { width: 48px; height: 48px; }
  .ppass .who { font-size: 1.06rem; }
}

/* ---- one day at a time -----------------------------------------------
   A three-day programme on a phone is a minute of flicking to answer
   "what is on after lunch". The strip cuts it to a day, and the count
   on each tab means nobody taps into an empty one to find out.        */
.pdays {
  display: flex; gap: 8px; flex-wrap: wrap;
  margin: 0 0 20px; padding-bottom: 2px;
}
.pdaytab {
  display: flex; align-items: baseline; gap: 8px;
  padding: 8px 14px; border: 1px solid var(--border); border-radius: 999px;
  color: var(--muted-fg); text-decoration: none; font-size: 0.86rem;
  transition: border-color .12s ease, color .12s ease, background .12s ease;
}
.pdaytab b { font-weight: 600; }
.pdaytab span {
  font-size: 0.72rem; font-variant-numeric: tabular-nums;
  padding: 1px 7px; border-radius: 999px; background: var(--muted);
}
.pdaytab:hover { color: var(--fg); border-color: var(--fg); text-decoration: none; }
.pdaytab.on {
  background: var(--primary); border-color: var(--primary); color: #fff;
}
.pdaytab.on span { background: rgba(255, 255, 255, .22); color: #fff; }
.pdaytab-mine { margin-left: auto; }
.pdaytab-mine.on { background: var(--success, #12a05e); border-color: var(--success, #12a05e); }

@media (max-width: 620px) {
  .pdays { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 8px; }
  .pdaytab { flex: 0 0 auto; }
  .pdaytab-mine { margin-left: 0; }
}

/* A long abstract is worth having and not worth six lines of every
   card. Folded by default; the summary row is the affordance.         */
.psess details.pdesc > summary {
  cursor: pointer; font-size: 0.82rem; color: var(--primary);
  font-weight: 600; margin-top: 6px; list-style: none;
}
.psess details.pdesc > summary::-webkit-details-marker { display: none; }
.psess details.pdesc > summary::after { content: " \25BE"; }
.psess details.pdesc[open] > summary::after { content: " \25B4"; }
.psess details.pdesc p.desc { margin-top: 6px; }

/* ---- a timetable is a list, not a stack of cards ----------------------
   Day one of this event has fifty-six sessions across thirty-seven start
   times, many five minutes apart. At that granularity a card per session
   is the wrong component: what people do with it is scan for a time and
   a room. Rows collapse the same markup to one line each, and open into
   the full card in place.                                              */
.pviewbar {
  display: flex; align-items: center; gap: 8px;
  margin: -8px 0 18px; font-size: 0.8rem; color: var(--muted-fg);
}
.pviewbar > span { margin-right: auto; font-variant-numeric: tabular-nums; }
.pview {
  padding: 4px 12px; border: 1px solid var(--border); border-radius: 999px;
  color: var(--muted-fg); text-decoration: none;
}
.pview:hover { color: var(--fg); text-decoration: none; }
.pview.on { background: var(--fg); border-color: var(--fg); color: var(--bg); }

.pslot.rows { margin-bottom: 8px; }
.pslot.rows > h3 { margin-bottom: 4px; }

.psessrow { border-bottom: 1px solid var(--border); }
.psessrow > summary {
  display: flex; align-items: baseline; gap: 12px;
  padding: 9px 2px; cursor: pointer; list-style: none;
  font-size: 0.9rem;
}
.psessrow > summary::-webkit-details-marker { display: none; }
.psessrow > summary:hover { background: var(--muted); }
.psessrow .rt {
  flex: 0 0 auto; min-width: 3.4em; font-variant-numeric: tabular-nums;
  color: var(--muted-fg); font-size: 0.82rem;
}
.psessrow .rtitle {
  flex: 1 1 auto; min-width: 0; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.psessrow .rroom {
  flex: 0 0 auto; font-size: 0.76rem; color: var(--muted-fg);
  max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.psessrow .rmine { flex: 0 0 auto; color: var(--success, #12a05e); font-weight: 700; }
.psessrow.on > summary { background: color-mix(in srgb, var(--success, #12a05e) 8%, transparent); }
.psessrow[open] > summary { background: var(--muted); }
.psessrow[open] .rtitle { white-space: normal; }
/* The card inside a row has already been announced by the summary. */
.psessrow > .psess { border: 0; border-radius: 0; margin: 0 0 10px; padding: 4px 2px 12px; }
.psessrow > .psess.on { box-shadow: none; }
.psessrow > .psess h4 { display: none; }

@media (max-width: 560px) {
  .psessrow .rroom { display: none; }
}

/* ---- looking at a document without leaving the page ------------------
   Tapping "Receipt" used to put a PDF in the downloads folder and the
   attendee on a blank viewer with nowhere to go but Back — and on a
   phone, Back out of a PDF is a gamble. The document opens above the
   programme instead, with the keeping of it as a separate, deliberate
   button.                                                              */
.pdoc {
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--card); overflow: hidden; margin-bottom: 20px;
}
.pdoc-bar {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 10px 14px; border-bottom: 1px solid var(--border);
  background: var(--bg-alt);
}
.pdoc-bar b { margin-right: auto; font-size: 0.95rem; }
.pdoc-frame {
  display: block; width: 100%; height: min(72vh, 780px); border: 0;
  background: var(--muted);
}
.pdoc-frame p { padding: 20px; }

@media (max-width: 560px) {
  .pdoc-frame { height: 60vh; }
  .pdoc-bar b { flex: 1 0 100%; margin-bottom: 4px; }
}

/* The little mark that says "this one leaves the page". Quiet, because
   it is a reassurance and not a feature.                               */
.plink .pnew {
  font-style: normal; font-size: 0.8em; opacity: .5; margin-left: 4px;
  vertical-align: super;
}

/* ---- the live board --------------------------------------------------
   Read from the back of a room, or held in one hand while walking a
   building. Dark because it lives on a wall for eight hours, and large
   because the question it answers is asked from a distance.           */
.live-body { background: #0d0b12; color: #f4f2f7; margin: 0; }
.live-wrap { max-width: 1400px; margin: 0 auto; padding: 22px 24px 40px; }

.live-head {
  display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap;
  padding-bottom: 16px; border-bottom: 1px solid #262230; margin-bottom: 16px;
}
.live-head h1 { font-size: 1.7rem; margin: 0; line-height: 1.15; }
.live-head p { margin: 4px 0 0; color: #9d97ae; font-size: 0.9rem; }
.live-nums { display: flex; gap: 26px; margin-left: auto; text-align: right; }
.live-num b { display: block; font-size: 1.7rem; line-height: 1.1; font-variant-numeric: tabular-nums; }
.live-num span { font-size: 0.76rem; color: #9d97ae; }

/* The one thing that must never be ambiguous: is this screen alive.
   An empty board and a dead board look identical from ten feet away. */
.live-state::before {
  content: ""; display: inline-block; width: 7px; height: 7px;
  border-radius: 50%; margin-right: 6px; vertical-align: 1px;
}
.live-state.is-live::before { background: #12a05e; box-shadow: 0 0 0 0 rgba(18,160,94,.7); animation: livePulse 2s infinite; }
.live-state.is-wait::before { background: #d08700; }
@keyframes livePulse {
  70%  { box-shadow: 0 0 0 7px rgba(18, 160, 94, 0); }
  100% { box-shadow: 0 0 0 0 rgba(18, 160, 94, 0); }
}

.live-rooms { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
.live-roomchip {
  display: flex; align-items: baseline; gap: 7px;
  background: #171420; border: 1px solid #262230; border-radius: 999px;
  padding: 6px 14px;
}
.live-roomchip b { font-size: 1rem; font-variant-numeric: tabular-nums; }
.live-roomchip span { font-size: 0.78rem; color: #9d97ae; }

.live-list { display: flex; flex-direction: column; }
.live-row {
  display: flex; align-items: center; gap: 16px;
  padding: 12px 6px; border-bottom: 1px solid #1d1926;
}
.live-row.is-new { animation: liveIn .55s ease; }
@keyframes liveIn {
  from { opacity: 0; transform: translateY(-8px); background: rgba(18,160,94,.14); }
  to   { opacity: 1; transform: none; background: transparent; }
}
.live-row.is-refused { opacity: .55; }
.live-row.is-refused .live-where b { color: #e07a6a; }

.live-face {
  flex: 0 0 auto; width: 46px; height: 46px; border-radius: 50%;
  background: #221d2e; display: grid; place-items: center; overflow: hidden;
  font-weight: 700; font-size: 1.1rem; color: #b9b2cc;
}
.live-face img { width: 100%; height: 100%; object-fit: cover; }

.live-who { flex: 1 1 auto; min-width: 0; }
.live-who b { display: block; font-size: 1.12rem; line-height: 1.2;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-who span { display: block; font-size: 0.82rem; color: #9d97ae;
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.live-where { flex: 0 0 auto; text-align: right; max-width: 38%; }
.live-where b { display: block; font-size: 0.95rem; font-weight: 600;
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.live-where span { font-size: 0.78rem; color: #9d97ae; font-variant-numeric: tabular-nums; }

.live-empty { padding: 60px 20px; text-align: center; color: #9d97ae; }
.live-empty p { margin: 0 0 6px; font-size: 1.05rem; }
.live-empty .small { font-size: 0.85rem; }

.live-foot { margin-top: 28px; display: flex; gap: 18px; font-size: 0.82rem; }
.live-foot a { color: #9d97ae; }

@media (max-width: 700px) {
  .live-wrap { padding: 16px 14px 30px; }
  .live-head h1 { font-size: 1.25rem; }
  .live-nums { margin-left: 0; gap: 18px; }
  .live-num b { font-size: 1.3rem; }
  .live-face { width: 38px; height: 38px; font-size: 0.95rem; }
  .live-who b { font-size: 1rem; }
  .live-where { max-width: 32%; }
}
