/* ==========================================================================
   Self-hosted faces. Converted from the supplied OTFs to WOFF2 (fonttools).
   Lab Grotesque : 816 glyphs — full Latin. Safe for UI and body.
   FT System     : 66 glyphs — A-Z a-z 0-9 . , ONLY. Display use only, and it
                   cannot set most of this app's copy. See CONTEXT.md.
   ========================================================================== */
@font-face { font-family: "Lab Grotesque"; src: url("assets/fonts/lab-grotesque-400.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Lab Grotesque"; src: url("assets/fonts/lab-grotesque-700.woff2") format("woff2"); font-weight: 700; font-style: normal; font-display: swap; }
@font-face { font-family: "Lab Grotesque Mono"; src: url("assets/fonts/lab-grotesque-mono-400.woff2") format("woff2"); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: "Lab Grotesque Mono"; src: url("assets/fonts/lab-grotesque-mono-700.woff2") format("woff2"); font-weight: 700; font-style: normal; font-display: swap; }

/* FT System Blank, trial cut. It carries 66 glyphs: space, comma, period, the
   digits and the Latin alphabet. No accents, no question mark, no apostrophe,
   no en dash, no middot, no currency. The unicode-range is not a nicety here —
   without it the browser picks a fallback per glyph at render time and the
   result is unpredictable. With it, FT System claims exactly what it has and
   Lab Grotesque takes the rest, deterministically. */
@font-face { font-family: "FT System"; src: url("assets/fonts/ft-system-400.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
  unicode-range: U+0020, U+002C, U+002E, U+0030-0039, U+0041-005A, U+0061-007A; }
@font-face { font-family: "FT System"; src: url("assets/fonts/ft-system-500.woff2") format("woff2");
  font-weight: 500; font-style: normal; font-display: swap;
  unicode-range: U+0020, U+002C, U+002E, U+0030-0039, U+0041-005A, U+0061-007A; }
@font-face { font-family: "FT System"; src: url("assets/fonts/ft-system-600.woff2") format("woff2");
  font-weight: 600; font-style: normal; font-display: swap;
  unicode-range: U+0020, U+002C, U+002E, U+0030-0039, U+0041-005A, U+0061-007A; }
@font-face { font-family: "FT System"; src: url("assets/fonts/ft-system-700.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
  unicode-range: U+0020, U+002C, U+002E, U+0030-0039, U+0041-005A, U+0061-007A; }

:root {
  /* Every page starts at the same x: the rail's width plus the page gutter.
     Views without a rail leave the gutter empty rather than pulling left. */
  /* Device safe areas. env() is correct on a real handset; the fallbacks match
     an iPhone 17's status bar and home indicator so the prototype sits where the
     shipped app would. */
  /* Shape. Navigation started as the one rounded family and the product has
     followed it, so the radius is a token rather than an exception. */
  --r-pill: 999px;
  --r-field: 16px;
  /* Alpha ladder. Overlays and text on media were using twenty six different
     alphas; five steps cover every case, so "white at 60" means one thing. */
  --a-1: .12;  --a-2: .24;  --a-3: .40;  --a-4: .60;  --a-5: .82;
  /* Frost: one tint and one blur for every full-screen overlay. */
  --frost-tint: rgb(232 232 232 / .34);
  --frost-blur: blur(34px) saturate(118%);
  --r-card: 12px;  /* map nodes */
  --r-chip: 10px;  /* icon chips */
  --safe-top: max(env(safe-area-inset-top), 52px);
  --safe-bottom: max(env(safe-area-inset-bottom), 30px);
  --rail-w: 190px;
  --page-gutter: clamp(24px, 6vw, 90px);
  /* documentation grid: racks share one gap, prose columns share another */
  --doc-gap: 16px;
  --doc-col-gap: 40px;

  /* ------------------------------------------------------------------
     Maestro brand — the "Maestro Brand Colors" board is canonical.
     Six values, exactly as specified. Where the D2 artwork differs
     (#EFEFEF for gray, #201E1A for off-black) the BOARD wins.
     Provenance + contrast table: 01_Operations/03_Clients/04_Active/
     fictive-kin/05_Deliverables/maestro/brand-token-layer/
     ------------------------------------------------------------------ */

  /* The six */
  --off-white:   oklch(1.0000 0 0);            /* #FFFFFF */
  --gray:        oklch(0.9530 0.0035 85);      /* #EDEBE8 warm fill */
  --off-black:   oklch(0.1730 0 0);            /* #101010 */
  --accent:      oklch(0.9240 0.0984 116.9);   /* #E1EFA3 lemon */
  --purple:      oklch(0.7840 0.0870 301.2);   /* #C2ACE8 light purple */
  --brand:       oklch(0.2349 0.0713 24.5);    /* #390C0C burgundy red */

  /* Roles, bound to the six. Names unchanged so existing usage keeps working. */
  /* The app ground is a warm off-white rather than pure white, and the fills
     sit a step off it. A neutral that carries a trace of the brand's warmth
     reads as chosen; #FFFFFF next to #EAEAEA reads as unset. Separation is
     carried by these two grounds, which is why --line is now barely used. */
  --surface: oklch(0.9880 0.0022 85);          /* #FBFAF9 warm paper */
  --canvas:  oklch(0.9720 0.0030 85);          /* #F5F3F1 page behind it */
  --ink:     var(--off-black);
  --raised:  oklch(0.9600 0.0035 85);          /* #F0EEEB filled row */
  --muted:   oklch(0.3942 0 0);                /* #464646 */
  --quiet:   oklch(0.5280 0 0);
  --line:    oklch(0.9100 0.0030 85 / .9);
  --pressed: oklch(0.9330 0.0040 85);

  /* On burgundy */
  --on-brand:       var(--off-white);
  --on-brand-muted: var(--gray);
  --brand-raised:   oklch(0.3191 0.0604 21.0);
  --brand-hovered:  oklch(0.3191 0.0604 21.0);
  --brand-pressed:  oklch(0.1750 0.0560 24.5);
  --gray-hovered:   oklch(0.8950 0 0);
  --gray-pressed:   oklch(0.8550 0 0);
  /* Map state colors come from the brand palette, not from invented hues. The
     four states the map needs are already in it: lemon, burgundy, purple and a
     neutral for what is not built. Glyph color is set per state because lemon
     and purple need ink on them and burgundy needs white. */
  /* Map states, on the app's ramp. The brand marks the one entry point and
     nothing else; everything after it is carried by the gray steps, the same
     way the app carries state by weight rather than by hue. */
  --s-entry:  var(--c-maestro-red);
  --s-built:  var(--c-black);
  --s-shared: var(--c-gray-500);
  --s-todo:   var(--c-gray-300);
  --chassis:        oklch(0.12 0 0);            /* device shell, not a brand color */
  --media-ground:   oklch(0.22 0 0);            /* behind photography before it loads */
  --hairline:       oklch(1 0 0 / .5);          /* light hairline on tinted grounds */
  --brand-scrim:    oklch(0.2349 0.0713 24.5 / .5);

  /* Semantics. Lemon measures 1.23:1 on white, so it is never a mark on the
     white ground — it is legal on burgundy or off-black only. */
  --live:     oklch(0.55 0.15 30);
  --signal:   oklch(0.52 0.13 25);
  --notation: var(--purple);
  --harmony:  var(--ink);

  --sans: "FT System", "Lab Grotesque", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
  --mono: "Lab Grotesque Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  /* The display face. There is no serif in this system — this is FT System
     Blank. Lab Grotesque sits behind it purely as a glyph fallback: FT System
     Trial carries only A-Z a-z 0-9 . , so accents, colons, dashes, apostrophes
     and question marks would otherwise drop to a system font. */
  /* FT System Blank, Formula Type. 66 glyphs: the alphabet, the digits, space,
     comma and period. Everything else in the product — accents, question marks,
     apostrophes, en dashes, the middot separator, currency — falls to Lab
     Grotesque by unicode-range. See the Typography section for what that costs. */
  --display: "FT System", "Lab Grotesque", ui-sans-serif, sans-serif;
  --ease: cubic-bezier(.22, 1, .36, 1);
}

* { box-sizing: border-box; }
html {
  /* Some views scroll and some do not. With a classic scrollbar that changes
     the viewport width, which moves anything centered — including the nav.
     Reserve the gutter so the bar cannot shift between views. */
  scrollbar-gutter: stable; background: var(--canvas); color: var(--ink); font-family: var(--sans); }
body { margin: 0; min-width: 320px; }
button { color: inherit; font: inherit; }
/* One focus treatment, everywhere: 2px ink, offset 2px. Five rules ran at 3px
   and one at 2px, and the input stacked an inset ring on top of its outline, so
   focus read as a heavy double frame rather than a state. */
button:focus-visible { outline: 1.5px solid var(--harmony); outline-offset: 2px; }
[hidden] { display: none !important; }
.icon {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  flex: none;
}
.icon-chevron { width: 15px; height: 15px; color: var(--quiet); }

.prototype-bar {
  min-height: 64px;
  padding: 12px 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-bottom: 0;
  background: transparent;
  position: sticky;
  top: 0;
  z-index: 20;
}
.prototype-mark { display: inline-flex; align-items: center; margin-right: 12px; color: var(--brand); }
/* One outlined lockup, referenced by <use>, inheriting currentColor so it works
   on white and on burgundy. Source: assets/maestro-lockup.svg (511x92). */
/* The lockup ships with 8 units of clearspace padded into its viewBox, because
   the artwork itself has none: its ink sits on the box edge. Ratio follows the
   padded box, not the artwork. */
.lockup { display: block; width: auto; aspect-ratio: 135 / 42; fill: currentColor; }
.prototype-mark .lockup { height: 18px; }
.splash-stack .lockup { height: 34px; }
.identity-intro h1 .lockup { height: 46px; }
/* Mark slots inside the app. Home speaks as the product; the pass is the one
   artifact a member hands to another person, so it carries the mark too.
   Organization codes (CSO on event IDs) stay in the data, not in these slots. */
.app-header > .lockup { height: 13px; flex: none; margin-top: 7px; color: var(--brand); }
.pass-top .lockup { height: 11px; flex: none; color: currentColor; }
.prototype-meta { color: var(--muted); font-size: 12px; }
/* The one rounded family in the system is navigation: the in-app bar is a pill,
   so the view switch is the same object at desk scale rather than a second
   exception. The mark sits inside the pill, so the bar carries one object
   centered rather than two things pushed to opposite edges. */
/* One spacing value everywhere: the pill's padding and every gap inside it are
   the same 8px, so the mark is inset from the edge exactly as far as the tabs
   are from each other. No drop shadow — the hairline and the blur separate it
   from the page. */
.nav-pill { display: flex; align-items: center; gap: 26px;
  padding: 8px 8px 8px 22px; border-radius: var(--r-pill);
  background: color-mix(in oklch, var(--off-white) 60%, transparent);
  backdrop-filter: blur(26px) saturate(170%);
  -webkit-backdrop-filter: blur(26px) saturate(170%);
  box-shadow: none; }
.nav-pill .prototype-mark { margin: 0; flex: none; display: inline-flex;
  align-items: center; color: var(--brand); }
.nav-pill .prototype-mark .lockup { height: 15px; }
/* One grid track per view, all 1fr, so every segment is the width of the
   longest label instead of the width of its own words. */
.view-switch { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 8px; padding: 0; border: 0; border-radius: var(--r-pill);
  background: transparent; }
.view-switch button { position: relative; min-height: 40px; border: 0;
  border-radius: var(--r-pill); padding: 0 18px; font-family: var(--sans);
  font-size: 11px; font-weight: 500; letter-spacing: .005em;
  white-space: nowrap; cursor: pointer;
  color: var(--muted); background: transparent;
  box-shadow: none;
  transition: color 160ms ease, background-color 220ms var(--ease),
    box-shadow 220ms var(--ease); }
.view-switch button:hover { color: var(--ink);
  background: rgb(255 255 255 / .60); }
/* The active tab must not change weight. Tracks are sized from the widest
   label, so bolding a long one grew every track, which resized the pill, which
   moved it — the bar drifted as you tabbed around. The white fill and the ink
   color carry the state instead. */
.view-switch button.is-active { color: var(--ink);
  background: var(--off-white); box-shadow: none; }
.view-switch button:focus-visible { outline: 1.5px solid var(--harmony);
  outline-offset: 2px; }


.stage {
  min-height: calc(100vh - 64px);
  padding: 24px clamp(20px, 6vw, 96px) 32px;
  display: grid;
  /* The track, not the item, was the problem: an auto column sizes to the
     phone's max-content width, which its aspect-ratio makes 424px. On a 390px
     screen that put 34px of every view off the right edge with no way to reach
     it. minmax(0, 1fr) lets the column be narrower than its content wants. */
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
}
.phone {
  width: auto;
  aspect-ratio: 402 / 874;
  height: min(874px, calc(100vh - 78px));
  min-height: 700px;
  border: 8px solid var(--chassis);
  border-radius: 55px;
  background: var(--surface);
  position: relative;
  overflow: hidden;
  box-shadow: 0 30px 90px rgb(20 20 18 / .14), inset 0 0 0 1px var(--hairline);
}
.screen { height: 100%; overflow: auto; scrollbar-width: none;
  padding: calc(var(--safe-top) + 6px) 18px 104px; }
.screen::-webkit-scrollbar { display: none; }
.splash-screen {
  position: absolute;
  /* inset already sizes this to the padding box; the extra width/height was
     resolving against a different box and pushing 7px past the frame. */
  inset: 0;
  border-radius: 47px;
  padding: 0;
  overflow: hidden;
  background: #080808;
  color: white;
}
.splash-screen > img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  /* The canon frame is landscape and the device is portrait, so cover crops the
     sides. Hold the hand and the bow, which sit center-right of the frame. */
  object-position: 62% center;
  filter: contrast(1.04);
  /* Matches the 2s hold: an animation that outlives the screen never lands. */
  animation: splash-focus 2s var(--ease) both;
}
.splash-shade {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to bottom, rgb(0 0 0 / .18) 0%, transparent 34%),
    linear-gradient(to top, rgb(0 0 0 / .86) 0%, rgb(0 0 0 / .08) 46%, transparent 68%);
}
.splash-stack {
  position: absolute;
  left: 22px;
  right: 22px;
  top: 50%;
  transform: translateY(-42%);
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
.splash-stack p {
  margin: 10px 0 0;
  color: rgb(255 255 255 / .82);
  font-size: 15px;
  letter-spacing: .005em;
}
/* No button: the splash advances on its own and the whole screen is the
   fallback control, so nothing needs a visible affordance. */
.splash-screen { cursor: pointer; }
/* A button that sits on a photograph: white ground, ink label, same pill. */
/* Buttons that sit on a photograph are glass, not paint: translucent white over
   a blur, the same treatment the navigation pill uses, so the image reads through
   them instead of being blocked out. */
/* Glass, not paint. At 72% over a bright part of the photograph it read as a
   solid white pill; at 52% with a heavier blur the image moves through it and a
   hairline gives the edge definition the lower opacity costs. */
.btn--onmedia { background: color-mix(in srgb, var(--off-white) 52%, transparent);
  color: var(--ink); backdrop-filter: blur(26px) saturate(180%);
  -webkit-backdrop-filter: blur(26px) saturate(180%);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .40); }
.btn--onmedia:active { background: color-mix(in srgb, var(--off-white) 70%, transparent); }
.app-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 26px; padding-top: 22px; }
.app-header.simple { align-items: center; }
.date { color: var(--muted); font-size: 12px; letter-spacing: .07em; text-transform: uppercase; margin: 0 0 9px; }
.app-header h1, .sheet-page h1 { font: 36px/1.0 var(--display); letter-spacing: -.028em; margin: 0; font-weight: 400; }
.app-header.simple h1 { font-size: 32px; }
.app-header.simple .date { margin-bottom: 6px; }
.avatar, .round-action {
  width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--hairline); background: var(--raised); cursor: pointer; flex: none; transition: transform 160ms ease, background-color 180ms ease;
}
.avatar:active, .round-action:active { transform: scale(.94); }
.avatar { font-size: 12px; font-weight: 650; }


.status-strip { min-height: 38px; border: 1px solid var(--hairline); border-radius: 0; display: flex; align-items: center; gap: 8px; padding: 0 12px; background: var(--raised); font-size: 10px; margin-bottom: 12px; }
.status-strip strong { margin-left: auto; font-weight: 600; }
.status-glyph { width: 15px; height: 15px; color: var(--signal); stroke-width: 1.8; }

.feature-art, .event-hero { height: 168px; position: relative; display: flex; align-items: center; justify-content: center; background: linear-gradient(145deg, var(--media-ground), var(--media-ground)); color: white; overflow: hidden; }
.feature-art::after { content: ""; position: absolute; inset: 0; background: linear-gradient(to bottom, rgb(10 8 4 / .40), transparent 48%, rgb(10 8 4 / .12)); pointer-events: none; }
.feature-image { width: 100%; height: 100%; object-fit: cover; object-position: center 57%; transition: transform 700ms var(--ease), filter 700ms var(--ease); }
.feature:hover .feature-image { transform: scale(1.018); filter: saturate(1.04); }
.event-hero::before { content: ""; position: absolute; width: 210px; height: 210px; border: 1px solid var(--hairline); border-radius: 50%; transform: translate(-80px, 45px); box-shadow: 56px -38px 0 -1px transparent, 56px -38px 0 0 rgb(255 255 255 / .24); }
.feature-letter { font: italic 110px/.8 var(--display); opacity: .92; transform: translateY(-2px); animation: letter-drift 8s ease-in-out infinite alternate; }
.event-id { position: absolute; z-index: 1; left: 31px; top: 16px; font-size: 11px; letter-spacing: .05em; opacity: .92; }
.feature-copy { padding: 15px 16px 17px; }
.feature-copy p, .event-copy > p:first-child { margin: 0 0 4px; color: var(--muted); font-size: 13px; }
.feature-copy h2 { font: 22px/1.04 var(--display); margin: 0 0 6px; letter-spacing: -.025em; font-weight: 400; }
.feature-copy > span { color: var(--muted); font-size: 11px; display: block; }
.primary-action, .secondary-action { border: 0; min-height: 44px; border-radius: var(--r-pill); padding: 0 17px; cursor: pointer; font-size: 12px; letter-spacing: .025em; font-weight: 600; transition: transform 150ms ease, opacity 150ms ease; }
.primary-action:active, .secondary-action:active { transform: scale(.985); opacity: .86; }
.primary-action { color: white; background: var(--brand); }
.secondary-action { background: var(--pressed); }
.feature-copy .primary-action { width: 100%; margin-top: 14px; }

.section-block { margin-top: 24px; }
.section-heading { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 9px; }
.section-heading h2 { font-size: 13px; letter-spacing: .025em; margin: 0; }
.section-heading span, .section-heading button { color: var(--muted); border: 0; background: none; font-size: 11px; }
.section-heading button { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: flex-end; padding: 0 4px; margin: -14px -4px -14px 0; cursor: pointer; }
.action-row { width: 100%; min-height: 68px; border: 1px solid var(--hairline); background: var(--raised); border-radius: 0; padding: 10px 12px; display: grid; grid-template-columns: 38px 1fr auto; align-items: center; gap: 10px; text-align: left; cursor: pointer; transition: transform 180ms var(--ease), background-color 180ms ease; }
.action-row:active, .compact-grid button:active { transform: scale(.985); }
.row-icon { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 0; background: var(--pressed); font-size: 19px; }
.action-row strong, .action-row small { display: block; }
.action-row strong { font-size: 13px; }
.action-row small { color: var(--muted); margin-top: 3px; font-size: 11px; }
.compact-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.compact-grid button { border: 1px solid var(--hairline); background: var(--raised); border-radius: 0; min-height: 82px; padding: 13px; text-align: left; cursor: pointer; transition: transform 180ms var(--ease); }
.compact-grid strong { display: block; font: 25px/1 var(--display); margin-bottom: 8px; font-weight: 400; }
.compact-grid span { font-size: 11px; color: var(--muted); }

.bottom-nav { position: absolute; left: 0; right: 0; bottom: 0; min-height: 72px; background: var(--gray); border: 0; border-radius: 0; padding: 0 0 env(safe-area-inset-bottom, 0px); display: grid; grid-template-columns: repeat(4, 1fr); z-index: 5; box-shadow: none; }
.bottom-nav button { border: 0; background: none; border-radius: 0; font-size: 12px; color: var(--quiet); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; cursor: pointer; transition: color 180ms ease, transform 160ms ease; }
.bottom-nav button .icon { width: 22px; height: 22px; stroke-width: 1.6; }
.bottom-nav button:active { transform: scale(.94); }
/* current tab is tinted, not underlined -- the iOS convention, and it keeps
   the no-lines direction intact. */
.bottom-nav button.is-active { background: none; color: var(--brand); font-weight: 650; }
.bottom-nav button.is-active .icon { stroke-width: 2; }

.segments { display: flex; background: var(--gray); border: 0; border-radius: var(--r-pill); padding: 3px; margin-bottom: 14px; }
.segments button { flex: 1 1 0; min-width: 0; border: 0; background: none; min-height: 32px; padding: 0 12px; border-radius: var(--r-pill); font-size: 11px; letter-spacing: .025em; color: var(--muted); white-space: nowrap; transition: background-color 180ms ease, color 180ms ease; }

.segments button.is-active { background: var(--off-white); color: var(--ink); font-weight: 650; }


.detail-bar { min-height: 44px; display: grid; grid-template-columns: 44px 1fr 44px; align-items: center; text-align: center; margin: -5px 0 10px; font-size: 12px; letter-spacing: .02em; font-weight: 600; }
.detail-bar button { width: 44px; height: 44px; border: 0; background: transparent; font-size: 22px; cursor: pointer; }
.event-hero { margin: 0 -16px; height: 220px; }
/* Scrim instead of a text-shadow under .event-id. Fades to nothing well inside
   its own box so it never meets the image on a hard 1px line. */
.event-hero::after { content: ""; position: absolute; inset: 0 0 auto; height: 92px;
  background: linear-gradient(to bottom, rgb(0 0 0 / .55) 0%, rgb(0 0 0 / .28) 42%, rgb(0 0 0 / 0) 100%);
  pointer-events: none; }
.event-copy { padding-top: 18px; }
.event-copy h1 { font: 32px/.98 var(--display); margin: 0 0 12px; letter-spacing: -.03em; font-weight: 400; }
.event-meta { color: var(--muted); font-size: 12px; line-height: 1.55; }
.inline-actions { display: flex; gap: 8px; margin: 20px 0 28px; }
.program { border-top: 1px solid var(--line); padding-top: 18px; }
.program h2 { font-size: 15px; margin: 0 0 10px; }
.program div { display: flex; justify-content: space-between; padding: 11px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
.program span { color: var(--muted); }

.sheet-page { padding-top: 14px; }
.sheet-page > h1 { margin-bottom: 26px; max-width: 10ch; }
.choice { width: 100%; min-height: 78px; border: 0; border-top: 1px solid var(--line); background: transparent; display: grid; grid-template-columns: 46px 1fr auto; gap: 12px; align-items: center; text-align: left; cursor: pointer; }
.choice:last-of-type { border-bottom: 1px solid var(--line); }
.choice-icon { width: 46px; height: 46px; border-radius: var(--r-field); background: var(--raised); display: grid; place-items: center; font-size: 20px; }
.choice strong, .choice small { display: block; }
.choice strong { font-size: 15px; }
.choice small { color: var(--muted); font-size: 12px; margin-top: 4px; }
.support-note { color: var(--muted); font-size: 11px; line-height: 1.5; padding: 14px; background: var(--raised); border: 1px solid var(--hairline); border-radius: 0; margin-top: 18px; }

.tier { background: color-mix(in oklch, var(--signal), white 86%); color: var(--brand); border-radius: 0; padding: 7px 10px; font-size: 11px; font-weight: 700; }

.benefit-list { border-top: 1px solid var(--line); }
.benefit-list button { width: 100%; min-height: 78px; border: 0; border-bottom: 1px solid var(--line); background: transparent; display: flex; justify-content: space-between; align-items: center; text-align: left; cursor: pointer; }
.benefit-list strong, .benefit-list small { display: block; }
.benefit-list strong { font-size: 15px; }
.benefit-list small { color: var(--muted); font-size: 12px; margin-top: 4px; }
.benefit-list button > span:last-child { background: var(--pressed); border-radius: var(--r-pill); padding: 8px 11px; font-size: 12px; }

.profile-block { display: flex; align-items: center; gap: 12px; padding: 4px 0 24px; }
.large-avatar { width: 56px; height: 56px; border-radius: 50%; background: var(--pressed); display: grid; place-items: center; font: 20px/1 var(--display); }
.profile-block strong, .profile-block small { display: block; }
.profile-block strong { font-size: 16px; }
.profile-block small { color: var(--muted); font-size: 12px; margin-top: 4px; }
.settings-list { border-top: 1px solid var(--line); }
.settings-list button { width: 100%; min-height: 54px; padding: 0; border: 0; border-bottom: 1px solid var(--line); background: transparent; display: flex; justify-content: space-between; align-items: center; font-size: 14px; cursor: pointer; }
.settings-label { display: inline-flex; align-items: center; gap: 12px; }
.settings-label .icon { width: 16px; height: 16px; color: var(--muted); }

/* Same page margins as the identity and system views: the map is a page,
   not a canvas pinned to the window edge. Width stays uncapped so four columns
   keep their room. */
.map-view { padding: 70px var(--page-gutter) 96px; max-width: none; margin: 0; }
/* Header, legend and plate were on four different columns: 280, 90, 260, 290.
   One rule puts them all on the same one. */
/* The map has no subnav rail, so the 190px it reserved for one hung the whole
   diagram right of the nav pill, which is centered in the viewport. Centered to
   match. */
.map-view > .map-header,
.map-view > .map-legend,
.map-view > .sitemap-layout { margin-left: auto; margin-right: auto; max-width: 1300px; }
/* label sits above the title it labels; description is subordinate, not a peer */
.map-header { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, 380px);
  grid-template-areas: "label desc" "title desc"; column-gap: 48px; align-items: start; margin-bottom: 30px; }
.map-header p { grid-area: label; }
.map-header h1 { grid-area: title; }
.map-header > span { grid-area: desc; align-self: end; }
.map-header p { margin: 0 0 6px; font-family: var(--sans); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--quiet); }
.map-header h1 { font: 46px/1 var(--display); margin: 0; letter-spacing: -.025em; }
.map-header > span { color: var(--muted); font-size: 11px; line-height: 1.55; max-width: 46ch; }
.sitemap-layout { display: block; }
.sitemap-scroll { overflow-x: auto; padding-bottom: 18px; scrollbar-color: var(--line) transparent; }
.sitemap {
  width: 100%;
  max-width: 1180px;
  border-radius: var(--r-field);
  padding: 30px 30px 36px;
  border: 0;
  border-radius: 0;
  background-color: color-mix(in oklch, var(--raised) 60%, transparent);
  position: relative;
}
/* Cards after the org-chart reference: an icon in its own column, the name, a
   detail line, then the kind on a rule beneath. Smaller than before, and
   left-aligned, because centered text in a card this size reads as a label
   rather than a record. */
/* Two-part card, after the reference: a header row carrying the icon and the
   name, a hairline, then the detail in its own zone. The padding lives on the
   parts rather than the card, so a node with no detail is just the header. */
.site-node { width: 150px; min-height: 48px; padding: 0; border-radius: var(--r-card);
  display: grid; grid-template-columns: 35px minmax(0, 1fr); column-gap: 10px;
  row-gap: 0; align-content: start; text-align: left; border: 1px solid transparent;
  background: var(--raised); font-size: 11px; font-weight: 600;
  position: relative; z-index: 2; }
.site-node .node-ico { grid-column: 1; grid-row: 1; align-self: center;
  width: 24px; height: 24px; padding: 5px; box-sizing: border-box;
  border-radius: var(--r-chip); margin: 10px 0 10px 11px;
  color: var(--glyph, var(--off-white));
  background: var(--dot, var(--quiet)); }
.site-node .node-t { grid-column: 2; grid-row: 1; align-self: center;
  font-weight: 600; line-height: 1.25; padding: 10px 11px 10px 0; }
.site-node[data-kind] .node-t { padding-right: 74px; }
.site-node .sub { grid-column: 1 / -1; grid-row: 2; padding: 9px 11px 10px;
  border-top: 1px solid rgb(0 0 0 / .07); }
.site-node[data-route] { cursor: pointer; }
.site-node[data-route]:hover { background: var(--off-white); }
.site-node[data-route]:hover .node-ico { opacity: 1; }
.site-node[data-route]:focus-visible { outline: 1.5px solid var(--harmony); outline-offset: 2px; }

/* The four destinations are the spine of the app, so they read as a different
   class of thing: filled, not outlined, with the bottom-nav slot they occupy. */
.node-primary[data-nav-slot] { background: var(--ink); color: var(--off-white);
  border-color: var(--ink); }
.node-primary[data-nav-slot] .node-ico { color: var(--off-white);
  background: rgb(255 255 255 / .12); }
.node-primary[data-nav-slot]::before { content: "NAV " attr(data-nav-slot);
  position: absolute; top: 50%; transform: translateY(-50%); right: 12px;
  padding: 2px 8px; border-radius: var(--r-pill); background: rgb(255 255 255 / .12);
  font-family: var(--sans); font-size: 8.5px; font-weight: 600;
  letter-spacing: .07em; color: rgb(255 255 255 / .82); line-height: 1.5; }
.node-primary[data-nav-slot]:hover { background: var(--ink); }
.site-node small { margin-left: 7px; padding: 3px 5px; border-radius: 0; background: var(--ink); color: white; font-size: 7px; text-transform: uppercase; }
/* State is a badge, not a hairline. Four low-chroma borders at 1px were not
   telling each other apart, so each state now carries a filled dot in its own
   hue beside its label, and the hues are pushed far enough apart in hue angle
   to be distinguishable: amber 85, teal 195, violet 300, blue 250. */
.node-root     { border-color: var(--s-entry);  --dot: var(--s-entry);  --glyph: var(--ink); }
.node-detail   { border-color: var(--s-built);  --dot: var(--s-built);  --glyph: var(--off-white); }
.node-auth     { border-color: var(--s-shared); --dot: var(--s-shared); --glyph: var(--ink); }
.node-function { border-color: var(--s-todo);   --dot: var(--s-todo);   --glyph: var(--off-white); border-style: dashed; }
.node-root, .node-detail, .node-auth, .node-primary[data-nav-slot] { border-color: transparent; }
.node-primary { --dot: var(--off-white); }
/* The dot rides on the label's own background box, because ::before is the
   connector stem and ::after is the label. No third pseudo needed. */

.ia-children .site-node::before { content: ""; position: absolute; left: 50%;
  top: -26px; width: 0; height: 26px; margin-left: -0.5px;
  border-left: 1px dashed var(--quiet); }
.tree-root { display: flex; flex-direction: column; align-items: center; }
.root-line { height: 24px; border-left: 1px solid var(--quiet); }
.entry-path { display: flex; align-items: center; justify-content: center; gap: 8px; }
.tree-arrow { color: var(--quiet); font-size: 12px; transform: rotate(-90deg); }
.shell-trunk { height: 28px; border-left: 1px solid var(--quiet); }
.branch-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 28px; position: relative; padding-top: 1px; }
.branch-grid::before { content: ""; position: absolute; left: 110px; right: 110px; top: 0; border-top: 1px solid var(--quiet); }
.tree-branch { display: flex; flex-direction: column; align-items: center; position: relative; }
.branch-stem { height: 28px; border-left: 1px solid var(--quiet); }
.child-stack { margin-top: 22px; display: grid; gap: 20px; }
.child-stack .site-node::before { content: ""; position: absolute; left: 50%; top: -21px; height: 20px; border-left: 1px solid var(--quiet); }
.utility-row { margin-top: 54px; padding-top: 20px; border-top: 1px solid var(--quiet); display: flex; justify-content: flex-end; align-items: center; gap: 12px; }
.utility-row > span { color: var(--muted); font-size: 9px; margin-right: auto; }

.map-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 22px; margin: 0 0 22px;
  padding-top: 14px; border-top: 1px solid var(--line); }
.map-tools { margin-left: auto; display: flex; gap: 8px; }
.map-tools button { min-height: 34px; padding: 0 14px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--off-white); color: var(--ink);
  font-size: 11px; font-weight: 500; cursor: pointer;
  transition: background-color 160ms ease, border-color 160ms ease; }
.map-tools button:hover { background: var(--gray); border-color: var(--quiet); }
.map-tools button:hover { background: var(--gray); }
.map-tools button.is-done { background: var(--brand); color: var(--off-white); border-color: var(--brand); }
.legend-key { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 11px; }
.legend-key .swatch { width: 10px; height: 10px; border-radius: 50%; border: 0;
  background: var(--dot, var(--quiet)); flex: none; }
/* border-color is intentionally NOT set here: the shorthand would reset it to
   currentColor and outrank the .node-* classes, flattening every key to gray. */
.legend-key .swatch.node-function { border-style: solid; }
.legend-key .swatch.node-primary { background: var(--ink); }

.ia-root { display: flex; flex-direction: column; align-items: center; max-width: 1120px; margin-inline: auto; }
.ia-root .site-node { width: 190px; }
.ia-root-stem { height: 24px; border-left: 1px solid var(--quiet); }
.ia-columns { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; position: relative; padding-top: 1px;
  max-width: 1260px; margin-inline: auto; }
/* The rail stops at the center of the first and last column. With five columns
   and four 16px gaps that is (100% - 64px) / 10 from each edge: half a column,
   derived from the grid rather than guessed. */
.ia-columns::before { content: ""; position: absolute; left: calc((100% - 64px) / 10); right: calc((100% - 64px) / 10); top: 0; border-top: 1px solid var(--quiet); }
.ia-branch { display: flex; flex-direction: column; align-items: center; }
.ia-branch-stem { height: 24px; border-left: 1px solid var(--quiet); }
.ia-branch > .node-primary { width: 100%; min-height: 48px; border-radius: var(--r-pill);
  grid-template-columns: 24px minmax(0, 1fr); align-content: center;
  column-gap: 10px; padding: 8px 16px; }
.ia-branch > .node-primary .node-ico { grid-row: 1; align-self: center; }
.ia-branch > .node-primary .node-t { align-self: center; }
.ia-branch > .node-primary .node-t { padding-right: 0; }
.node-primary[data-nav-slot]::before { right: 14px; }
.ia-children { display: grid; gap: 26px; margin-top: 22px; align-content: start; width: 100%; }
.ia-children .site-node { width: 100%; }
.ia-children .site-node { --stem: 1; }
.ia-children .site-node::marker { content: ""; }
.ia-children .site-node { width: 100%; }
.sitemap .site-node { line-height: 1.25; }
.sitemap .site-node .sub { font-size: 9.5px; font-weight: 450; letter-spacing: 0; color: var(--muted); text-transform: none; line-height: 1.35; }
.ia-band { margin-top: 52px; padding-top: 0; border-top: 0; max-width: 1120px; margin-inline: auto; }
.ia-band header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 16px; gap: 16px; }
.ia-band header span { color: var(--muted); font-size: 10px; }
.ia-band header strong { font-size: 11px; letter-spacing: .04em; text-transform: uppercase; }
.ia-feature-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.ia-feature-grid .site-node { width: 100%; }
.ia-shared { grid-template-columns: repeat(3, 1fr); }

.ia-utilities { margin-top: 40px; border-top-color: var(--quiet); }
.ia-utilities .site-node { font-weight: 500; color: var(--muted); }

/* ---- Typed nodes, after the flow-canvas reference -----------------------
   In the reference every node carries a small label above it saying what kind
   of step it is, sitting on the connector so it breaks the line. Adopted here
   for the types that are easy to misread — the entry, the four destinations,
   the shared screens and anything not built. The ordinary in-prototype screen
   stays unlabelled: repeating one word down thirty nodes is noise, not signal.
   Curved edges and card shadows did not come across: they carry six nodes and
   turn to spaghetti at forty-five, and the system has neither radii nor
   shadows. */
.site-node[data-kind]::after {
  content: attr(data-kind);
  position: absolute;
  top: 9px;
  right: 10px;
  padding: 2px 7px;
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--dot, var(--quiet)) 15%, transparent);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--dot, var(--quiet)) 55%, var(--ink));
  white-space: nowrap;
  line-height: 1.5;
}


/* Direction on the spine only. Six arrowheads read as flow; forty-five read as
   clutter, and a top-down tree is not ambiguous about which way it runs. */
.ia-root-stem, .ia-branch-stem { position: relative; }
.ia-root-stem::after, .ia-branch-stem::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -1px;
  width: 5px;
  height: 5px;
  border-right: 1px solid var(--quiet);
  border-bottom: 1px solid var(--quiet);
  transform: translate(-50%, 0) rotate(45deg);
}

.identity-view { display: grid; grid-template-columns: 190px minmax(0, 1fr); align-items: start; }
.identity-subnav {
  position: sticky;
  top: 64px;
  height: calc(100vh - 64px);
  padding: 30px 18px;
  /* No plate and no rule. The rail is a list on the page, not a panel beside
     it: the quiet type and the space around it do the separating, so any fill
     here reads as a lighter column laid over the canvas. */
  background: none;
}
.identity-subnav > span { display: block; color: var(--muted); font-size: 10px; margin: 0 0 24px 21px; }
/* A quiet list, not a set of tabs. No plate: the current section is marked by
   a dot in its own gutter, everything else sits back in --quiet. Numerals stay
   because the sections are an ordered sequence, but they carry no emphasis. */
.identity-subnav nav { display: grid; gap: 12px; }
.identity-subnav a { position: relative; padding: 0 0 0 21px; display: block;
  border-radius: 0; color: var(--quiet); text-decoration: none;
  font-size: 15px; line-height: 1.3; transition: color 160ms ease; }
.identity-subnav a:hover { color: var(--muted); }
.identity-subnav a.is-current { color: var(--ink); font-weight: 500; }
.identity-subnav a.is-current::before { content: ""; position: absolute;
  left: 4px; top: .52em; width: 5px; height: 5px; border-radius: 50%;
  background: currentColor; }
.identity-content { width: 100%; max-width: calc(1180px + var(--page-gutter) * 2); margin: 0;
  padding: 70px var(--page-gutter) 96px; }
.identity-content [id] { scroll-margin-top: 90px; }
.identity-intro { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, 380px);
  grid-template-areas: "label desc" "title desc"; column-gap: 48px; align-items: start; }
.identity-intro > p { grid-area: label; }
.identity-intro h1 { grid-area: title; }
.identity-intro > div { grid-area: desc; align-self: end; }
.identity-intro > p { margin: 0 0 8px; font-family: var(--sans); font-size: 10px; letter-spacing: .12em; text-transform: uppercase; color: var(--quiet); }
.identity-intro h1 { margin: 0; font: 76px/.86 var(--display); letter-spacing: -.035em; }
.identity-intro > div { display: grid; gap: 10px; }
.identity-intro strong { font: 22px/1.08 var(--display); font-weight: 400; }
.identity-intro span { color: var(--muted); font-size: 11px; line-height: 1.55; max-width: 48ch; }
/* An intro with no description column does not reserve one. The grid holds
   "label desc" / "title desc" and a minmax(220px, 380px) second track, so
   dropping the div without this leaves a 380px gutter and squeezes the h1 into
   the first column for no reason. Scoped by absence rather than by id, so the
   identity intro — which still has its description — is untouched. */
.identity-intro:not(:has(> div)) {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "label" "title";
}
.identity-rule { display: none; }
.identity-section { display: grid; grid-template-columns: 180px 1fr; gap: 28px; padding: 44px 0; }
.identity-section > header { display: grid; grid-template-columns: 30px 1fr; align-content: start; }
.identity-section > header span { color: var(--quiet); font: 15px/1 var(--display); }
.identity-section > header h2 { margin: 0; font-size: 12px; }
.principle-list { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--doc-col-gap); }
.principle-list article { min-height: 96px; padding: 14px 0; }
.principle-list b { font-size: 13px; }
.principle-list p { margin: 6px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
.type-system { grid-template-columns: 180px 1fr 1fr; }
.type-specimen { padding-top: 12px; }
.type-specimen > span { color: var(--muted); font-size: 11px; }
.display-specimen p { margin: 38px 0 0; font: 64px/.9 var(--display); letter-spacing: -.035em; }
.ui-specimen p { margin: 48px 0 4px; font-size: 24px; font-weight: 650; letter-spacing: -.025em; }
.ui-specimen small { color: var(--muted); font-size: 10px; }
.color-rack { display: grid; grid-template-columns: repeat(6, 1fr); min-height: 190px; }
.color-rack article { padding: 14px; display: flex; flex-direction: column; justify-content: space-between; border: 1px solid var(--line); border-right: 0; font-size: 10px; }
.color-rack article:last-child { border-right: 1px solid var(--line); }
.color-rack code { font: 9px var(--sans); opacity: .6; }
.swatch-ink { background: var(--ink); color: white; }
.swatch-stage { background: var(--off-white); }
.swatch-lobby { background: var(--gray); }
.swatch-brand { background: var(--brand); color: white; }
.swatch-purple { background: var(--purple); color: var(--ink); }
.swatch-accent { background: var(--accent); color: var(--ink); }
.component-board { display: grid; grid-template-columns: 1fr 1fr; gap: var(--doc-gap); }
.component-board > article { padding: 16px; background: var(--raised); display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start; gap: 18px; }
.component-label { color: var(--muted); font-size: 11px; }
.identity-status { width: 100%; margin: 0; }
.icon-specimens { display: grid; grid-template-columns: repeat(12, 1fr); gap: var(--doc-gap); align-items: center; }
.icon-specimens .icon { width: 21px; height: 21px; }
.voice-card p { margin: 0 0 10px; font: 23px/1 var(--display); }
.voice-card small { color: var(--muted); font-size: 11px; line-height: 1.4; max-width: 42ch; }
.token-pair > div, .icon-system > div { min-width: 0; }
.spacing-rack, .shape-rack { display: grid; grid-template-columns: repeat(5, 1fr); }
.spacing-rack article, .shape-rack article { min-height: 150px; padding: 16px 12px 12px; border-right: 1px solid var(--line); display: grid; grid-template-rows: 1fr auto auto; align-items: end; }
.spacing-rack article:last-child, .shape-rack article:last-child { border-right: 0; }
.spacing-rack i { display: block; width: var(--size); height: var(--size); max-width: 48px; max-height: 48px; background: var(--signal); align-self: center; justify-self: center; }
.spacing-rack code, .shape-rack code { font: 11px var(--mono); }
.spacing-rack span, .shape-rack span { color: var(--muted); font-size: 11px; margin-top: 4px; }
.shape-rack { grid-template-columns: repeat(4, 1fr); }
.shape-rack i { width: 64px; height: 64px; align-self: center; justify-self: center; background: var(--pressed); border: 1px solid var(--line); }
.shape-tile { border-radius: var(--r-field); }
.shape-control { border-radius: var(--r-pill); }
.shape-card { border-radius: 0; }
.shape-round { border-radius: 50%; }
.token-note { color: var(--muted); font-size: 12px; margin: 22px 0 0; }
.token-note code { color: var(--ink); font: 11px var(--mono); }
.voice-system .voice-card { padding: 20px 20px 22px; background: var(--raised); }
@media (max-width: 900px) {
  /* The pill cannot wrap without stopping being a pill, so past the point where
     five segments fit it scrolls inside its own shape. Without this the last
     two views run off-screen and cannot be reached at all. */
  .prototype-bar { gap: 12px; }
  /* Centered, the pill would overflow both edges once the segments stop fitting,
     taking the mark off-screen on the left. Cap it to the bar and let the
     segments scroll inside it; the mark stays put. */
  .nav-pill { max-width: 100%; min-width: 0; }
  .nav-pill .prototype-mark { flex: none; }
  .view-switch { min-width: 0; overflow-x: auto; scrollbar-width: none;
    -ms-overflow-style: none; -webkit-overflow-scrolling: touch; }
  .view-switch::-webkit-scrollbar { display: none; }
  .view-switch button { flex: none; }

  .prototype-meta { display: none; }
  .stage { padding: 18px; align-items: flex-start; }
  .phone { height: calc(100vh - 100px); max-height: 844px; }
  .map-header { grid-template-columns: 1fr; gap: 10px; }
  .sitemap-layout { grid-template-columns: 1fr; }
  .identity-intro { grid-template-columns: 1fr; align-items: start; }
  .identity-section, .type-system { grid-template-columns: 1fr; }
  .identity-view { grid-template-columns: 150px minmax(0, 1fr); }
  .identity-content { padding-inline: 26px; }
  .spacing-rack { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 740px) {
  .view-switch { mask-image: linear-gradient(to right, #000 0, #000 90%, transparent 100%); }
}

@media (max-width: 520px) {
  .prototype-bar { padding: 9px 10px; }
  .prototype-mark .lockup { height: 15px; }
  .view-switch button { padding: 0 9px; }
  .stage { padding: 0; }
  .phone { width: 100%; min-width: 0; height: calc(100vh - 55px); min-height: 600px; border: 0; border-radius: 0; }
  .screen { height: calc(100% - 76px); padding-top: 18px; }
  .map-view { padding: 28px 12px 60px; }
  .map-header h1 { font-size: 34px; }
  .identity-view { display: block; }
  .identity-subnav { top: 55px; height: auto; padding: 7px 10px; border-right: 0; border-bottom: 1px solid var(--line); overflow-x: auto; z-index: 10; }
  .identity-subnav > span { display: none; }
  .identity-subnav nav { display: flex; width: max-content; }
  .identity-subnav a { min-height: 32px; padding: 0 10px; font-size: 12px;
    display: flex; align-items: center; }
  .identity-subnav a.is-current::before { display: none; }
  .identity-subnav nav { gap: 0; }
  .identity-content { padding: 38px 16px 64px; }
  .identity-intro h1 { font-size: 58px; }
  .principle-list, .component-board { grid-template-columns: 1fr; }
  .color-rack { grid-template-columns: 1fr 1fr; }
  .color-rack article { min-height: 110px; border-right: 1px solid var(--line); }
  .spacing-rack, .shape-rack { grid-template-columns: 1fr 1fr; }
}

.screen.entering { animation: screen-in 280ms var(--ease); }
@keyframes screen-in {
  from { opacity: 0; transform: translateY(7px) scale(.995); }
  to { opacity: 1; transform: none; }
}
@keyframes letter-drift {
  from { transform: translate3d(-3px, -2px, 0) rotate(-1deg); }
  to { transform: translate3d(4px, 1px, 0) rotate(.7deg); }
}
@keyframes splash-focus {
  from { transform: scale(1.035); filter: contrast(1.06) blur(2px); }
  to { transform: scale(1); filter: contrast(1.06) blur(0); }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* Lab Grotesque Mono is now available; token specimens use it. */
code, .identity-view code, .type-specimen code { font-family: var(--mono); }

/* Hit areas reach 44px without changing what is drawn. */
.view-switch button::after,
.identity-subnav a::after { content: ""; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 32px; }

/* Photography — specimens run wide so the falloff is visible; captions follow
   the house rule for captions: monospace, all caps. */
.photo-lede { max-width: 46ch; margin: 0 0 22px; font-size: 15px; line-height: 1.5; color: var(--ink); }
/* The frames are the argument, so they run the full container width and cross
   back over the label column rather than sitting in the text measure. */
.photo-system .photo-canon, .photo-system .photo-specimens {
  margin-left: calc(-1 * (180px + 28px)); }
/* Specimens that do not meet the brief are marked, not quietly shown as if they do. */
.photo-specimens figure.is-offbrief img { opacity: 1; }
.photo-specimens figure.is-offbrief figcaption { color: var(--brand); }
.photo-canon { margin: 0 0 22px; }
.photo-canon img { display: block; width: 100%; height: auto; border-radius: 0; }
.photo-canon figcaption { margin-top: 8px; font-family: var(--sans); font-size: 10px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink); }
.photo-specimens { display: grid; grid-template-columns: 1fr 1fr; gap: var(--doc-gap); margin-bottom: 28px; }
.photo-specimens figure { margin: 0; }
.photo-specimens img { display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover; border-radius: 0; }
.photo-specimens figcaption { margin-top: 8px; font-family: var(--sans); font-size: 10px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--quiet); }
.photo-rules { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--doc-col-gap); }
.photo-rules article { padding: 14px 0; }
.photo-rules b { font-size: 13px; }
.photo-rules p { margin: 6px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }

/* ---- Events: grouped by month, each event stating its own facts ---------
   Separation is a change of ground, not a rule. Metadata is mono. Every card
   declares the member's relationship to the event, which is the thing the row
   format could not say. */
.event-groups { display: flex; flex-direction: column; gap: 24px; }
.event-month { margin: 0 0 10px; font-family: var(--sans); font-size: 11px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--quiet); }
.event-card { background: var(--raised); padding: 14px; display: grid;
  grid-template-columns: 38px 1fr; gap: 14px; margin-bottom: 8px; }
.event-card:last-child { margin-bottom: 0; }
.event-when { text-align: center; padding-top: 2px; }
.event-when b { display: block; font: 23px/1 var(--display); color: var(--ink); }
.event-when span { display: block; margin-top: 3px; font-family: var(--sans);
  font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--quiet); }
.event-body { min-width: 0; }
.event-time { margin: 0; font-family: var(--sans); font-size: 11px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.event-title { display: block; width: 100%; margin: 5px 0 0; padding: 0; border: 0;
  background: none; text-align: left; font: 17px/1.15 var(--display);
  color: var(--ink); cursor: pointer; }
.event-venue { margin: 6px 0 0; font-family: var(--sans); font-size: 11px; color: var(--muted); }
.event-rel { margin: 10px 0 0; font-size: 12px; font-weight: 650; color: var(--ink); }
.event-rel.is-open { font-weight: 400; color: var(--muted); }
.event-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 11px; }
.event-actions button { min-height: 40px; padding: 0 16px; border: 0; border-radius: var(--r-pill);
  background: var(--gray); color: var(--ink); font-family: var(--sans); font-size: 11px;
  letter-spacing: .04em; cursor: pointer; transition: background-color 160ms ease; }
.event-actions button.is-primary { background: var(--brand); color: var(--off-white); }

/* ---- Mark + Grayscale (mirroring id-idler's section vocabulary) --------- */
.mark-specimens { display: grid; grid-template-columns: 1fr 1fr; gap: var(--doc-gap); margin-bottom: 24px; }
.mark-plate { display: grid; place-items: center; min-height: 132px; padding: 24px; background: var(--raised); color: var(--brand); }
.mark-plate.is-brand { background: var(--brand); color: var(--off-white); }
.mark-plate .lockup { height: 34px; }
.mark-rules { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--doc-col-gap); }
.mark-rules article { padding: 14px 0; }
.mark-rules b { font-size: 13px; }
.mark-rules p { margin: 6px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }

.gray-rack { display: grid; grid-template-columns: repeat(6, 1fr); gap: 1px; }
.gray-rack article { display: flex; flex-direction: column; gap: 0; }
.gray-rack i { display: block; height: 76px; }
.gray-rack code { margin-top: 9px; font-family: var(--mono); font-size: 11px; color: var(--ink); }
.gray-rack span { margin-top: 3px; font-size: 11px; color: var(--quiet); line-height: 1.4; }

.token-group { margin: 0 0 12px; font-family: var(--sans); font-size: 11px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--quiet); }
.token-system .token-group:not(:first-child) { margin-top: 30px; }

/* ---- Accessibility · Composition · Motion ------------------------------ */
.ratio-rack { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--doc-gap); margin: 16px 0 26px; }
.ratio-rack article { padding: 12px 14px; background: var(--raised); display: flex; flex-direction: column; gap: 4px; }
.ratio-rack b { font: 19px/1 var(--display); font-variant-numeric: tabular-nums; color: var(--ink); }
.ratio-rack span { font-size: 11px; line-height: 1.4; color: var(--muted); }
.ratio-rack .is-pass b, .ratio-rack article.is-pass b { color: var(--ink); }
.ratio-rack article.is-fail { background: color-mix(in oklch, var(--brand) 8%, transparent); }
.ratio-rack article.is-fail b { color: var(--brand); }

.access-rules { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--doc-col-gap); }
.access-rules article { padding: 14px 0; }
.access-rules b { font-size: 15px; }
.access-rules p { margin: 6px 0 0; color: var(--muted); font-size: 13px; line-height: 1.5; }

.comp-rack { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--doc-gap); margin-bottom: 24px; }
.comp-rack article { padding: 12px 14px; background: var(--raised); display: flex; flex-direction: column; gap: 3px; }
.comp-rack b { font: 19px/1 var(--display); font-variant-numeric: tabular-nums; }
.comp-rack code { font-family: var(--mono); font-size: 11px; color: var(--ink); }
.comp-rack span { font-size: 11px; color: var(--muted); line-height: 1.4; }

/* numbered rail, standards-site convention: number and label, number quiet */
.identity-subnav a { font-variant-numeric: tabular-nums; }

/* Every section's body starts on the same line as its label. Bodies begin with
   different elements -- a rack, a note, a bare div -- each carrying its own top
   margin, so without this no two sections aligned the same way. */
.identity-section > :not(header) { margin-top: 0; }
.identity-section > :not(header) > :first-child { margin-top: 0; }
.identity-section > :not(header) > :first-child.token-note { margin-top: 0; }

/* ---- Token table (Atlassian naming convention, Maestro values) ---------- */
.tok-rack { display: flex; flex-direction: column; margin-bottom: 30px; }
.tok-rack article { display: grid; grid-template-columns: 200px minmax(112px, 132px) 132px 1fr;
  gap: var(--doc-gap); align-items: center; padding: 9px 0; }
.tok-rack code { font-family: var(--mono); font-size: 12px; color: var(--ink); }
.tok-rack i { font-style: normal; display: flex; align-items: center; white-space: nowrap; overflow: hidden; }
.tok-rack b { font-family: var(--mono); font-size: 11px; font-weight: 400;
  color: var(--muted); font-variant-numeric: tabular-nums; }
.tok-rack span:last-child { font-size: 12px; color: var(--muted); line-height: 1.4; }
.tok-chip { width: 26px; height: 18px; display: block; box-shadow: inset 0 0 0 1px var(--line); }
.tok-bar { height: 8px; display: block; background: var(--brand); }
.token-table .token-group:not(:first-of-type) { margin-top: 26px; }

/* ---- Controls ----------------------------------------------------------
   .is-hover / .is-active / .is-focus force a state so the documentation can
   show every one at once. Real interaction uses the pseudo-classes beside them. */
.btn { display: inline-flex; white-space: nowrap; align-items: center; justify-content: center;
  min-height: 44px; padding: 0 22px; border: 0; border-radius: var(--r-pill);
  font-family: var(--sans); font-size: 13px; font-weight: 600; letter-spacing: .03em;
  cursor: pointer; transition: background-color 160ms ease, color 160ms ease; }
.btn:focus-visible, .btn.is-focus { outline: 1.5px solid var(--harmony); outline-offset: 2px; }

.btn--primary { background: var(--brand); color: var(--off-white); }
.btn--primary:active, .btn--primary.is-hover { background: var(--brand-hovered); }
.btn--primary:active, .btn--primary.is-active { background: var(--brand-pressed); }

.btn--secondary { background: var(--gray); color: var(--ink); width: 100%; }
.btn--secondary:active, .btn--secondary.is-hover { background: var(--gray-hovered); }
.btn--secondary:active, .btn--secondary.is-active { background: var(--gray-pressed); }

.btn--tertiary { background: transparent; color: var(--ink); padding: 0 12px; }
.btn--tertiary:active, .btn--tertiary.is-hover { background: var(--gray); }
.btn--tertiary:active, .btn--tertiary.is-active { background: var(--gray-pressed); }

.btn:disabled, .btn.is-disabled { background: var(--raised); color: var(--quiet); cursor: not-allowed; }
.btn--tertiary:disabled, .btn--tertiary.is-disabled { background: transparent; }

.field { display: flex; flex-direction: column; gap: 6px; width: 100%; }
.field label { font-family: var(--sans); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--quiet); }
.field input { min-height: 46px; width: 100%; padding: 0 16px; border: 0; border-radius: var(--r-field);
  background: var(--gray); color: var(--ink); font-family: var(--sans); font-size: 15px;
  transition: background-color 160ms ease, box-shadow 160ms ease; }
.field input::placeholder { color: var(--quiet); }
.field input:focus-visible, .field input.is-hover { background: var(--gray-hovered); }
.field input:focus, .field input.is-focus { outline: 1.5px solid var(--harmony); outline-offset: 2px; background: var(--off-white); box-shadow: none; }
.field input:disabled, .field input.is-disabled { background: var(--raised); color: var(--quiet); cursor: not-allowed; }
.field.is-error input { box-shadow: inset 0 0 0 1px var(--brand); background: var(--off-white); }
.field .field-msg { font-size: 12px; color: var(--brand); }
.field-set { display: grid; gap: 14px; margin-top: 4px; }
.section-block:has(.field-set) .section-heading { margin-bottom: 16px; }

.ctl-matrix { display: flex; flex-direction: column; gap: 34px; }
.ctl-row { display: grid; grid-template-columns: 104px repeat(5, 1fr); gap: 20px; align-items: start; }
.ctl-row > .ctl-name { font-size: 12px; font-weight: 650; color: var(--ink); padding-top: 13px; }
.ctl-cell { display: flex; flex-direction: column; gap: 10px; align-items: stretch; }
.ctl-cell > :first-child { align-self: stretch; }
.ctl-cell .btn--tertiary, .ctl-cell .btn--primary { align-self: flex-start; }
.ctl-cell i { font-style: normal; font-family: var(--sans); font-size: 10px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--quiet); }
.ctl-cell .field { width: 100%; }

/* About has no subnav rail, so there is nothing to align a left edge to. The
   reading column centers in the viewport instead: no rail offset on the view,
   and no max-width capping it off-center. */
/* The whole page sits at the muted gray rather than near-black. It is a
   read-through document, not a surface anyone acts on, so nothing in it needs
   to rank above the rest. --muted is 9.44:1 on this ground, well past AA. */

/* ---- Register R2: components added for the P0 items -------------------- */
.patron-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center;
  width: 100%; border: 0; border-radius: 0; padding: 12px; margin-bottom: 14px;
  background: var(--raised); text-align: left; cursor: pointer; }
.patron-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.patron-body strong { font-size: 13px; }
.patron-body small { font-size: 12px; color: var(--muted); }
.patron-meta { font-family: var(--sans); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--brand); }

.announce-rail { display: flex; gap: 8px; overflow-x: auto; margin-bottom: 14px;
  scrollbar-width: none; }
.announce-rail::-webkit-scrollbar { display: none; }
.announce { flex: 0 0 78%; text-align: left; border: 0; border-radius: 0; cursor: pointer;
  background: var(--gray); padding: 11px 12px; display: flex; flex-direction: column; gap: 3px; }
.announce em { font-style: normal; font-size: 11px; font-weight: 700; }
.announce span { font-size: 12px; line-height: 1.4; color: var(--muted); }

.own-line { margin: 8px 0 0; font-size: 11px; font-weight: 650; }
.own-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.own-actions .btn { min-height: 36px; font-size: 12px; padding: 0 12px; width: auto; }

.filter-row { display: flex; gap: 8px; overflow-x: auto; margin-bottom: 10px;
  scrollbar-width: none;
  /* The row bleeds into both gutters and fades across exactly those 18px, so a
     chip is only ever dimmed once it has scrolled out of the content column.
     A percentage stop cannot do this: it moves with the row's width and starts
     eating the chip that is still in view. */
  padding-left: 18px; padding-right: 18px; margin-left: -18px; margin-right: -18px;
  mask-image: linear-gradient(to right, transparent 0, #000 18px,
    #000 calc(100% - 18px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 18px,
    #000 calc(100% - 18px), transparent 100%); }
.filter-row::-webkit-scrollbar { display: none; }
.chip { flex: none; min-height: 32px; padding: 0 13px; border: 0; border-radius: var(--r-pill); cursor: pointer;
  background: var(--gray); color: var(--muted); font-size: 12px; display: inline-flex;
  align-items: center; gap: 6px; white-space: nowrap; }
.chip.is-on { background: var(--ink); color: var(--off-white); }
.chip .icon { width: 13px; height: 13px; }
.result-count { margin: 0 0 16px; font-family: var(--sans); font-size: 11px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--quiet); }
.linkish { border: 0; background: none; padding: 0; cursor: pointer; color: var(--brand);
  font: inherit; text-decoration: underline; text-underline-offset: 2px; }
.event-card.is-unavailable .event-title { color: var(--muted); }

.plan-strip { display: flex; justify-content: space-between; align-items: center; gap: 12px;
  background: var(--raised); padding: 12px; }
.plan-strip b { display: block; font-size: 14px; }
.plan-strip small { font-size: 12px; color: var(--muted); }
.plan-flag { font-family: var(--sans); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--brand); text-align: right; }

.incl-list { list-style: none; margin: 0; padding: 0; }
/* Two up. At half width the title and its state cannot sit on one line, so the
   state moves above the title and reads as the card's eyebrow. */
.incl-list { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; align-items: stretch; }
.incl-list li { display: flex; flex-direction: column; justify-content: flex-start; gap: 6px;
  padding: 12px 13px 13px; background: var(--gray); border: 0;
  border-radius: 14px; font-size: 14px; line-height: 1.25; }
.incl-list li > span { min-width: 0; }
.incl-list em { order: -1; font-style: normal; font-family: var(--sans); font-size: 10.5px;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.incl-list em.is-done { color: var(--brand); }
.incl-list em.is-open { color: var(--quiet); }

.renew-block { background: var(--raised); padding: 14px; }
.renew-block p { margin: 0; font-size: 14px; line-height: 1.55; color: var(--muted); }
.renew-block b { color: var(--ink); }
.renew-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.renew-actions .btn { min-height: 38px; font-size: 12px; width: auto; padding: 0 16px; }

.stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; }
.stat-row > div { background: var(--raised); padding: 12px; display: flex; flex-direction: column; gap: 3px; }
.stat-row b { font: 24px/1 var(--display); }
.stat-row small { font-size: 11px; color: var(--muted); }

.journey { list-style: none; margin: 0; padding: 0; }
/* The season read as a strip rather than a list: each stop is a column, the
   month above the day, and the one you are on takes a filled chip. The show
   names are already listed under Included shows, so repeating them here was
   the same six rows twice. */
.journey { display: grid; grid-template-columns: repeat(6, 1fr); gap: 4px;
  margin: 0; padding: 0; list-style: none; }
.journey li { display: grid; justify-items: center; align-content: center;
  gap: 6px; padding: 9px 2px; border-radius: 9px; }
.journey em { font-style: normal; font-family: var(--sans); font-size: 10.5px;
  letter-spacing: .08em; text-transform: uppercase; color: var(--quiet); }
.journey b { font-family: var(--sans); font-size: 15px; font-weight: 500;
  color: var(--quiet); font-variant-numeric: tabular-nums; }
.journey li.is-done b { color: var(--ink); }
.journey li.is-now { background: var(--brand); }
.journey li.is-now em { color: rgb(255 255 255 / .62); }
.journey li.is-now b { color: var(--off-white); font-weight: 700; }
/* TBD sat 2px low against the numbers because it was set 3px smaller, which
   put a wobble at the end of an otherwise level row. Same size, same baseline;
   the muted color is enough to say it is not a date. */
.journey li.is-unset b { font-size: 15px; letter-spacing: .01em; }

.kv-list > div { display: flex; justify-content: space-between; align-items: baseline; gap: 14px;
  padding: 10px 12px; background: var(--raised); margin-bottom: 4px; font-size: 14px; }
.kv-list span { color: var(--muted); }
.kv-list b { font-weight: 600; text-align: right; }
.kv-list b.is-open { color: var(--quiet); font-weight: 400; }

.tok-rack code.is-copyable { cursor: pointer; }
.tok-rack code.is-copyable:hover { color: var(--brand); }
.tok-rack code.is-copyable:focus-visible { outline: 1.5px solid var(--harmony); outline-offset: 2px; }
.tok-rack code.is-copied { color: var(--brand); }

/* ---- Home, restructured -------------------------------------------------
   Surfaces are unfilled. Separation comes from space and the occasional
   hairline instead of a stack of gray blocks. Imagery runs edge to edge. */
.patron-card { background: none; padding: 14px 0 18px; margin-bottom: 0; }

.announce-rail { gap: 0; margin: 10px 0 0; }
/* A count, not an unread count. Ink rather than burgundy: burgundy is reserved
   for the primary action and the active tab, and a badge is neither. */
/* The mark and the bell sit on their own bar above the greeting, so the header
   holds one row of chrome and one row of address rather than mixing them. */
.app-header:has(.app-bar) { display: block; }
.app-bar { display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 22px; }
.app-bar .lockup { height: 15px; color: var(--brand); }
/* A bottom drawer inside the device, not the page: it is absolute to the phone
   so it sits over the screen the way a sheet does on iOS. Opens on the trigger,
   closes on the scrim, on Escape, and on every action inside it. */
.sheet { position: absolute; inset: 0; z-index: 40; display: flex; align-items: flex-end; }
.sheet-scrim { position: absolute; inset: 0; background: rgb(16 16 16 / .40);
  backdrop-filter: blur(2px); }
.sheet-panel { position: relative; width: 100%; background: var(--off-white);
  border-radius: 18px 18px 0 0; padding: 10px 18px 26px; animation: sheet-in 320ms var(--ease); }
.sheet-grip { width: 36px; height: 4px; border-radius: var(--r-pill); background: var(--line);
  margin: 0 auto 16px; }
.sheet-eyebrow { margin: 0 0 6px; font-family: var(--sans); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--quiet); }
.sheet-panel h2 { margin: 0 0 8px; font: 24px/1.1 var(--display); font-weight: 400;
  letter-spacing: -.02em; }
.sheet-body { margin: 0 0 16px; font-size: 14px; line-height: 1.5; color: var(--muted); }
.sheet-actions { display: grid; gap: 8px; margin-top: 14px; }
.sheet-actions .btn { width: 100%; min-height: 46px; }
.sheet-foot { margin: 14px 0 0; font-size: 13px; color: var(--muted); }
.attended-trigger { margin-top: 18px; width: 100%; min-height: 44px; }
@keyframes sheet-in { from { transform: translateY(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .sheet-panel { animation: none; } }


/* A tray, not a jump: the review asked for the carousel to open in place. Every
   opener needs a closer, so this one closes on outside click and on Escape. */
/* The full-screen frost, used for the member pass. It is the one thing a member
   holds up to another person, so it gets the whole device: no chrome, no nav,
   the facts large, one pill to dismiss. Announcements did not need this much
   screen and now open their own list instead. */
.stage-over { position: absolute; inset: 0; z-index: 45;
  background: var(--frost-tint);
  backdrop-filter: var(--frost-blur);
  -webkit-backdrop-filter: var(--frost-blur);
  animation: stage-fade 240ms var(--ease); }
@keyframes stage-fade { from { opacity: 0; } to { opacity: 1; } }
.stage-inner { position: absolute; inset: 0; display: flex; flex-direction: column;
  justify-content: center; padding: 0 26px 26px; color: var(--ink); }
.stage-eyebrow { margin: 0 0 10px; font-family: var(--sans); font-size: 12px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
.stage-name { margin: 0 0 26px; font: 38px/1.02 var(--display); font-weight: 400;
  letter-spacing: -.03em; }
.stage-facts { display: grid; gap: 16px; margin: 0; padding-top: 18px;
  border-top: 1px solid var(--line); }
.stage-facts div { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; }
.stage-facts dt { font-family: var(--sans); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--quiet); }
.stage-facts dd { margin: 0; font-family: var(--mono); font-size: 15px; text-align: right; }
.stage-note { margin: 22px 0 0; font-size: 13px; line-height: 1.5;
  color: var(--muted); max-width: 34ch; }
.stage-close { position: absolute; left: 50%; bottom: calc(var(--safe-bottom) + 8px);
  transform: translateX(-50%); display: inline-flex; align-items: center; gap: 8px;
  min-height: 46px; padding: 0 22px; border: 0; border-radius: var(--r-pill);
  background: var(--ink); color: var(--off-white); font-family: var(--sans);
  font-size: 14px; font-weight: 600; }
.stage-close .icon { width: 15px; height: 15px; }
.show-pass { width: 100%; margin-bottom: 16px; }
@media (prefers-reduced-motion: reduce) { .stage-over { animation: none; } }

.bell { position: relative; border: 0; background: none; padding: 0; cursor: pointer;
  width: 34px; height: 34px; display: grid; place-items: center; margin-right: -6px; }
.bell .icon { width: 19px; height: 19px; }

/* Home leads with the image running to the very top, chrome floating on it: the
   mark as a pill and the bell as a round button, the way the reference does. The
   screen drops its top padding so nothing sits above the photograph. */
/* The hero carries the media ground, not the screen. Putting it on the screen
   darkened everything below the photograph and turned the greeting and the rail
   headings into dark text on a dark field. */
.screen:has(> .hero-chrome) { padding-top: 0; }
/* Two fifths of the device. At a quarter the eyebrow ran under the logo pill;
   this gives the type its full size back and still leaves the majority of the
   screen to the membership content below. */
.hero { background: var(--media-ground);
  height: calc(min(874px, 100vh - 78px) * 0.40); min-height: 300px; }
.hero { position: relative; margin: 0 -18px; }
.hero-img { display: block; width: 100%; height: 100%; object-fit: cover;
  object-position: center 57%; background: var(--media-ground); }
.hero-veil { position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top, rgb(10 8 4 / .82) 0%, rgb(10 8 4 / .60) 30%,
    rgb(10 8 4 / .12) 62%, rgb(10 8 4 / .24) 100%); }
/* The chrome sticks for the whole screen rather than riding the hero, so the
   mark and the bell stay reachable at any scroll position. It sits above the
   content and pulls the hero back up underneath itself. */
.hero-chrome { position: sticky; top: 0; z-index: 20; display: flex;
  align-items: center; justify-content: space-between;
  padding: var(--safe-top) 0 12px;
  /* Pull the hero fully under the chrome. At 64 it only covered part of the
     chrome's 101px and left a white band above the photograph. No negative
     horizontal margin: .screen scrolls, so it clips anything outside its box,
     which was cutting the left edge off the mark pill. */
  margin: 0 0 calc(-1 * (var(--safe-top) + 52px));
  pointer-events: none; }
.hero-chrome > * { pointer-events: auto; }
.hero-chrome > * { pointer-events: auto; }
/* Chrome runs dark on the photograph, the way the tab bar does, so the two
   floating objects on screen belong to the same family. Translucent ink over a
   blur rather than flat black, so the image still moves under them. */
.mark-pill { display: inline-flex; align-items: center; height: 40px; padding: 0 18px;
  border-radius: var(--r-pill); color: var(--off-white);
  background: color-mix(in srgb, var(--off-black) 82%, transparent);
  backdrop-filter: blur(22px) saturate(170%);
  -webkit-backdrop-filter: blur(22px) saturate(170%); }
.mark-pill .lockup { height: 13px; }
.round-btn { position: relative; width: 40px; height: 40px; border: 0;
  border-radius: var(--r-pill); color: var(--off-white);
  background: color-mix(in srgb, var(--off-black) 82%, transparent);
  backdrop-filter: blur(22px) saturate(170%);
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  display: grid; place-items: center; }
.round-btn .icon { width: 18px; height: 18px; }
/* The alert is the one bright thing up here, which is what makes it read as an
   alert rather than as another piece of chrome. */
.hero-body { position: absolute; left: 18px; right: 18px; bottom: 16px; }
.hero-when { margin: 0 0 8px; font-family: var(--sans); font-size: 12px;
  letter-spacing: .1em; text-transform: uppercase; color: rgb(255 255 255 / .82); }
.hero-title { margin: 0; font: 30px/1.05 var(--display); font-weight: 400;
  letter-spacing: -.028em; color: var(--off-white); }
.hero-where { margin: 8px 0 0; font-size: 13px; color: rgb(255 255 255 / .82); }
.hero-cta { display: inline-flex; align-items: center; justify-content: center;
  width: auto; min-height: 42px; padding: 0 24px; margin: 14px 0 0; border: 0;
  border-radius: var(--r-pill); color: var(--ink);
  background: color-mix(in srgb, var(--off-white) 88%, transparent);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  font-family: var(--sans); font-size: 13px; font-weight: 600; cursor: pointer; }
.home-date { margin: 20px 0 4px; font-family: var(--sans); font-size: 12px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--quiet); }
.home-greet { margin: 0 0 4px; font: 24px/1.1 var(--display); font-weight: 400;
  letter-spacing: -.02em; color: var(--quiet); }

/* One statement of what they have: the package by name, the tier and tenure
   under it, then the four facts they would otherwise open a tab to find. The
   review asked for the subscription to be named specifically rather than left
   as a tier word. */
/* Memberships are a rail: a patron holds more than one relationship with the
   organization and each is its own card. Bleeds right so the next card peeks,
   which is how the rail says there is more without a control saying so. */

/* Mandatory snap, and the snap line sits on the gutter, so a card always lands
   on position instead of stopping wherever the flick ran out. align-items keeps
   every card the height of the tallest. */
.member-rail { display: flex; align-items: stretch; gap: 10px; overflow-x: auto;
  overflow-y: hidden; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-padding-left: 0;
  scrollbar-width: none; margin: 0 -18px 0 0; padding: 2px 18px 4px 0;
  width: calc(100% + 18px); }
.member-rail::-webkit-scrollbar { display: none; }
/* Fades where it scrolls, so the next card is a peek rather than a cut. Same
   treatment as the filter row and the announcement rail. */
.member-rail { mask-image: linear-gradient(to right, #000 0, #000 68%, rgb(0 0 0 / .25) 92%, transparent 100%);
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 68%, rgb(0 0 0 / .25) 92%, transparent 100%); }
/* A column, so the facts row can be pushed to the foot of every card and the
   boards line up across the rail whatever length the title above them ran to. */
.member-card { flex: 0 0 82%; scroll-snap-align: start; display: flex;
  flex-direction: column; text-align: left;
  border: 0; border-radius: 14px; background: var(--gray);
  padding: 15px 16px 17px; margin: 0; cursor: pointer; }
/* The issuer and the state share the top line, the way they do on a physical
   card: who issued it on the left, whether it is live on the right. The type
   of membership drops to its own line beneath, so the three labels sit in
   three ranks instead of competing across one row.

   The issuer is set as type. No mark exists for these institutions in the
   build, and drawing an approximation of a real logo is worse than naming it;
   the real asset should come from the client. */
/* The issuer's name wraps to two lines on some cards and one on others, which
   put the type, the title and the board at a different height on each. The name
   holds a two-line box on every card, so what follows starts at one height
   whatever the institution is called. */
.member-head { display: flex; justify-content: space-between; align-items: flex-start;
  gap: 12px; }
.member-head .member-state { margin-top: 1px; }
.member-issuer { display: grid; gap: 1px; }
.member-issuer b { font-family: var(--sans); font-size: 13px; font-weight: 750;
  letter-spacing: -.01em; color: var(--ink); }
.member-issuer em { font-style: normal; font-family: var(--sans); font-size: 8.5px;
  font-weight: 500; letter-spacing: .05em; text-transform: uppercase;
  color: oklch(0.5280 0 0); line-height: 1.25;
  /* One line. The two-line reserve was added when the cards were different
     widths; they are not, so it only ever held an empty second line open and
     put a hole between the issuer and the membership type. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%; }
.member-kicker { display: block; margin-top: 13px; font-family: var(--sans);
  font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--quiet); }
/* One hue for one state. Live takes the lemon; a cadence and a count are not
   states, so they stay neutral. */
.member-state { display: inline-flex; align-items: center; flex: none;
  padding: 4px 10px; border-radius: var(--r-pill); background: var(--pressed);
  font-family: var(--sans); font-size: 10.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); font-weight: 700;
  white-space: nowrap; }
/* The pill used to read "Active", which every membership in this rail is by
   definition — a lapsed one would not be shown. A label that never varies
   carries no information, and in the accent it was color spent on nothing.
   The slot is empty unless something is waiting on the member, so the lemon
   means one thing: this membership has an unclaimed benefit or a deadline. */
.member-state.is-due { background: color-mix(in srgb, var(--accent) 62%, white);
  color: oklch(0.4200 0.0640 108); }
.member-name { display: block; margin: 5px 0 4px; font: 21px/1.08 var(--display);
  font-weight: 400; letter-spacing: -.02em; }
.member-meta { display: block; font-size: 13px; color: var(--muted); }
.member-board { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px;
  margin-top: auto; padding-top: 16px; }
.member-board > span { display: block; min-width: 0; }
.member-board em { display: block; font-family: var(--sans); font-size: 10.5px;
  font-style: normal; letter-spacing: .1em; text-transform: uppercase; color: var(--quiet); }
.member-board b { display: block; margin-top: 4px; font-size: 14px; font-weight: 400;
  color: var(--ink); }


/* The three subscription blocks are one thing, so they nest inside a module
   rather than sitting in the section stack as peers of Household and Season
   journey. The module carries the outer heading; each block carries its own. */
.sub-module { margin-top: 18px; }
.sub-module + .sub-module { margin-top: 26px; }
.sub-module-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: 14px; margin-bottom: 8px; }
.sub-module-head h3 { margin: 0; font-family: var(--sans); font-size: 14px;
  font-weight: 650; letter-spacing: -.005em; text-transform: none; color: var(--ink); }
.sub-module-head span { color: var(--quiet); font-size: 12px; }

/* Grouped by when it matters rather than listed flat, and built on the same
   icon-row the settings and benefits lists use, so a notice reads like every
   other row in the app. No unread state: the register lists read and expiry
   behavior as undefined and blocked on the CMS. */
.notice-list { border-top: 0; }
.notice-item:last-child { border-bottom: 0; }
.notice-item { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto;
  align-items: start; gap: 12px; width: 100%; text-align: left; border: 0;
  border-bottom: 1px solid var(--line); background: none; padding: 15px 0;
  cursor: pointer; }
.notice-item.is-static { cursor: default; }
.notice-item:not(.is-static):active { background: var(--raised); }
.notice-ico { width: 26px; height: 26px; display: grid; place-items: center;
  background: var(--pressed); color: var(--ink); }
.notice-ico .icon { width: 14px; height: 14px; }
.notice-copy { display: grid; gap: 3px; min-width: 0; }
.notice-copy b { font-size: 15px; }
.notice-copy span { font-size: 13px; line-height: 1.45; color: var(--muted); }
.notice-item .icon-chevron { align-self: center; }

.section-heading h2 .linkish { font: inherit; letter-spacing: inherit;
  text-transform: inherit; color: inherit; padding: 0; text-decoration: none;
  display: inline-flex; align-items: center; gap: 5px; }
.section-heading h2 .linkish::after { content: ""; width: 5px; height: 5px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg); opacity: .55; }
.section-heading h2 .linkish:active { color: var(--ink); }
.feature-art { border: 0; padding: 0; cursor: pointer; width: 100%; }

/* After the reference: an image of the night, then what it is, when, where and
   what you hold. No rules between rows; the thumbnail already starts each one.
   The date takes the brand hue rather than the lemon, because this list sits on
   white where lemon is unreadable. */
.held-list { list-style: none; margin: 0; padding: 0; border-top: 0;
  display: grid; gap: 2px; }
.held-list li { display: block; padding: 0; border: 0; }
.held-list li > button { display: grid; grid-template-columns: 68px 1fr 16px;
  gap: 14px; align-items: center; width: 100%; padding: 8px 0; border: 0;
  background: none; text-align: left; cursor: pointer; }
.held-thumb { width: 68px; height: 68px; border-radius: 5px; object-fit: cover;
  background: var(--media-ground); }
.held-copy { display: grid; gap: 2px; min-width: 0; }
.held-list b { font-family: var(--sans); font-size: 14px; font-weight: 650;
  line-height: 1.25; letter-spacing: -.005em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; }
.held-list em { font-style: normal; font-family: var(--sans); font-size: 12px;
  font-weight: 600; color: var(--brand); }
/* One line for where, truncating rather than wrapping, so every row is the
   same height and the column of thumbnails stays even. */
.held-list small { color: var(--muted); font-size: 12px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.held-list .icon-chevron { color: var(--quiet); align-self: center; }
.section-heading .announce { flex: 0 0 82%; background: none; padding: 0 14px 0 0; }
.announce em { font-size: 11px; }



/* label column, the way the reference sets specs */
.section-heading h2 { font-family: var(--sans); font-size: 12px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--quiet); }
.section-heading span { font-size: 12px; }

/* The ask, the assist, then the utilities. Weight comes from the documented
   button atoms — filled, gray, unfilled — not from four underlined links of
   equal weight arranged as a keypad. */
.own-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 18px; }
.own-actions .btn { min-height: 46px; font-size: 13px; width: 100%; }
.own-actions .btn--tertiary { min-height: 40px; box-shadow: inset 0 0 0 1px var(--line); }

.action-row { background: none; border: 0; padding: 16px 0 0; }

/* ---- Stylization carried to the rest of the app -------------------------
   Content surfaces are unfilled and parted by a hairline. Controls keep a fill
   so they still read as pressable, and the pass card stays a solid object. */
.event-card { background: none; padding: 18px 0; margin-bottom: 0;
  border-bottom: 1px solid var(--line); }
.event-card:last-child { border-bottom: 0; }
.event-groups { gap: 8px; }
.event-month { margin: 18px 0 2px; }
.event-actions { gap: 18px; margin-top: 14px; }
.event-actions button { background: none; padding: 0; min-height: 32px;
  text-decoration: underline; text-underline-offset: 3px; }
.event-actions button.is-primary { background: var(--brand); color: var(--off-white);
  padding: 0 18px; min-height: 44px; text-decoration: none; }

.plan-strip { background: none; padding: 14px 0 18px; }

.renew-block { background: var(--gray); border-radius: 14px; padding: 15px 16px 16px; }
.renew-actions .btn--tertiary { text-decoration: underline; text-underline-offset: 3px; }

.stat-row { gap: 0; }
.stat-row > div { background: none; padding: 14px 0; border-bottom: 1px solid var(--line); }
.kv-list > div { background: none; padding: 13px 0; margin-bottom: 0;
  border-bottom: 1px solid var(--line); }
.kv-list > div:last-child { border-bottom: 0; }

.support-note { background: none; }
.profile-block { padding-bottom: 22px; }

/* ---- Floating navigation ------------------------------------------------
   The bar lifts off the screen and content passes beneath it, so the blur has
   something to act on. Kept square: the system has no corner radii, and a pill
   would be the only rounded object in the build. */
.bottom-nav {
  left: 14px;
  right: 14px;
  bottom: 14px;
  min-height: 64px;
  background: color-mix(in oklch, var(--off-white) 78%, transparent);
  backdrop-filter: blur(26px) saturate(150%);
  -webkit-backdrop-filter: blur(26px) saturate(150%);
  box-shadow: 0 12px 38px rgb(16 16 16 / .12), inset 0 0 0 1px oklch(0.173 0 0 / .10);
  padding: 0 0 env(safe-area-inset-bottom, 0px);
}
.bottom-nav button { font-size: 12px; gap: 4px; }
.bottom-nav button .icon { width: 21px; height: 21px; }
/* content clears the floating bar rather than running under its edge */
.screen { padding-bottom: 104px; }

/* Content dissolves under the floating pill instead of meeting it on a hard
   line. A blur, not a wash: the earlier white gradient painted a slab across the
   full-bleed hero. Masked so the blur ramps in rather than starting on an edge,
   which would just move the hard line up. */
.phone::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 104px; pointer-events: none; z-index: 4;
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  mask-image: linear-gradient(to bottom, transparent 0%, rgb(0 0 0 / .35) 34%, #000 72%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgb(0 0 0 / .35) 34%, #000 72%); }
/* The splash has no navigation, so it needs no dissolve. */
.phone:has(.splash-screen:not([hidden]))::after { display: none; }


/* ---- Navigation, pill ---------------------------------------------------
   Text only, content width, centered, floating over content. The current
   destination is a filled segment inside the pill rather than a color change.
   The one rounded object in the build, taken from the reference deliberately. */
.bottom-nav {
  left: 50%;
  right: auto;
  transform: translateX(-50%);
  bottom: 18px;
  width: max-content;
  max-width: calc(100% - 24px);
  min-height: 0;
  display: flex;
  gap: 2px;
  padding: 5px;
  border-radius: var(--r-pill);
  background: color-mix(in oklch, var(--ink) 82%, transparent);
  backdrop-filter: blur(26px) saturate(150%);
  -webkit-backdrop-filter: blur(26px) saturate(150%);
  box-shadow: 0 14px 40px rgb(16 16 16 / .24);
}
.bottom-nav button {
  flex: 0 0 auto;
  min-height: 40px;
  padding: 0 16px;
  border-radius: var(--r-pill);
  gap: 0;
  flex-direction: row;
  font-size: 14px;
  font-weight: 500;
  color: color-mix(in oklch, var(--off-white) 72%, transparent);
  white-space: nowrap;
}
.bottom-nav button .icon { display: none; }
.bottom-nav button.is-active {
  background: color-mix(in oklch, var(--off-white) 22%, transparent);
  color: var(--off-white);
  font-weight: 600;
}
.bottom-nav button:active { color: var(--off-white); }

/* ---- Tonight's event as a card ------------------------------------------
   The one contained object on Home. Image sits inside the card rather than
   bleeding, and the actions belong to it. Square, hairline edge, no fill, so
   it stays an object without reintroducing a gray block. */
.feature {
  margin-top: 22px;
  border: 1px solid var(--line);
  background: var(--off-white);
  overflow: hidden;
}
.feature-art { margin: 0; height: 208px; }
.feature-copy { padding: 16px 16px 18px; }
.feature-copy h2 { font-size: 22px; margin: 2px 0 6px; }
.event-id { left: 14px; top: 13px; }

/* The foot fade exists to dissolve content under the floating pill. The splash
   has no navigation, so it has no fade either, and the frame runs edge to edge. */
.splash-screen { z-index: 6; }

/* .splash-screen also carries .screen, so it was inheriting the content-screen
   height and padding meant for scrolling views. It fills the device instead. */
.screen.splash-screen,
.splash-screen { inset: 0; width: 100%; height: 100%; padding: 0; }
.splash-screen > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* Pill fills its container rather than hugging the labels, with the four
   destinations sharing the width evenly. Denser glass. */
.bottom-nav {
  left: 14px;
  right: 14px;
  transform: none;
  width: auto;
  max-width: none;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0;
  padding: 6px;
  background: color-mix(in oklch, var(--ink) 90%, transparent);
  backdrop-filter: blur(40px) saturate(170%);
  -webkit-backdrop-filter: blur(40px) saturate(170%);
  box-shadow: 0 16px 44px rgb(16 16 16 / .40);
}
/* The pill clears the home indicator, and content clears the pill. Both are
   derived from one safe-area token rather than a hand-tuned offset. */
.bottom-nav { bottom: var(--safe-bottom); }
.screen { padding-bottom: calc(104px + var(--safe-bottom)); }
.bottom-nav button { padding: 0 8px; min-height: 44px; font-size: 14px; }
/* content clears the pill: 44 + 12 padding + 18 inset + air */
.screen { padding-bottom: 112px; }

/* Dividers run the full width of the device while their text stays on the
   gutter, so a rule never stops short of the edge it is meant to reach. Only
   list peers are in here now — the one-off blocks are parted by space, and a
   width with no style left behind would resurface the moment one was set. */
.kv-list > div,
.stat-row > div { border-bottom-width: 1px; }

.kv-list > div, .stat-row > div,
.settings-list button, .benefit-list button,
.settings-list, .benefit-list, .held-list, .notice-list {
  margin-left: -18px;
  margin-right: -18px;
  padding-left: 18px;
  padding-right: 18px;
}
.stat-row { grid-template-columns: repeat(3, 1fr); }
.stat-row > div { margin: 0; padding-left: 0; padding-right: 0; }
/* These bled the outer figures to the device edge back when the row carried a
   fill. It does not any more — the row is three figures over a hairline — so
   the negative margins only pushed the last one 18px past the screen and left
   the whole screen laterally scrollable. */
.stat-row > div:first-child { margin-left: 0; padding-left: 0; }
.stat-row > div:last-child { margin-right: 0; padding-right: 0; }

/* the rail peeks the next card rather than looking clipped */
.announce-rail { margin-left: -18px; margin-right: -18px; padding-left: 18px;
  scroll-snap-type: x proximity; }
.announce { flex: 0 0 74%; scroll-snap-align: start; margin-right: 14px;
  padding-left: 0; padding-right: 0; }
.announce:last-child { margin-right: 18px; }

/* width:100% plus a negative margin shifts an element; it does not widen it.
   These need to grow by the gutter on both sides to reach the device edge. */
.kv-list > div,
.held-list, .notice-list {
  width: calc(100% + 36px);
  box-sizing: border-box;
}
/* The figures sit inside grid tracks now, not on the bleed, so a width that
   adds the gutter only overflows the track. */
.stat-row > div { width: 100%; box-sizing: border-box; }
.announce-rail { width: calc(100% + 36px); }

/* The rail should bleed right so the next card peeks, and stay on the gutter at
   the left. The earlier negative left margin pushed its first card off-screen. */
.announce-rail { margin-left: 0; margin-right: -18px; padding-left: 0;
  width: calc(100% + 18px); }
.announce { padding: 14px 0; margin-right: 14px; }
.announce:last-child { margin-right: 18px; }

/* Controls are gray-filled and the documentation page is gray, so the default
   state vanished while hover showed only because hover is darker. The specimen
   sits on white, which is the ground these controls actually appear on. */
.ctl-matrix { background: var(--off-white); padding: 22px 20px; }
.ctl-row { align-items: start; }
.ctl-cell { justify-content: flex-start; }
.ctl-cell > :first-child { align-self: stretch; }
/* tertiary is a text button: keep it flush left so it does not read as a
   mis-set filled button next to the others */
.ctl-cell .btn--tertiary { align-self: flex-start; }
.ctl-cell .field { width: 100%; }
.ctl-cell .field input { width: 100%; }

/* ---- Composition, drawn ------------------------------------------------- */
.map-plate { background: var(--off-white); padding: 20px; margin-bottom: 26px; }
.map-plate p { margin: 10px 0 0; font-size: 11px; color: var(--muted); line-height: 1.5; }
.map-plate code { font-family: var(--mono); font-size: 11px; color: var(--ink); }

.map-doc-grid { display: grid; grid-template-columns: 180px 28px 1fr; height: 64px; }
.map-col { display: grid; place-items: center; font-size: 10px; color: var(--quiet);
  box-shadow: inset 0 0 0 1px var(--line); }
.map-col.is-label { background: color-mix(in oklch, var(--brand) 9%, transparent); }
.map-gutter { display: grid; place-items: center; }
.map-gutter i { display: block; width: 1px; height: 100%;
  background: repeating-linear-gradient(var(--line) 0 4px, transparent 4px 8px); }
.map-dims { display: grid; grid-template-columns: 180px 28px 1fr; margin-top: 6px; }
.map-dims span { font-family: var(--mono); font-size: 9px; color: var(--quiet); text-align: center; }

.map-two { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
.map-gap-demo { display: flex; align-items: stretch; height: 40px; }
.map-gap-demo i { flex: 1; box-shadow: inset 0 0 0 1px var(--line); }
.map-gap-demo i.wide { flex: 1; }
.map-gap-demo b { background: color-mix(in oklch, var(--brand) 16%, transparent); }

.map-tree { position: relative; padding-top: 22px; }
.map-tree-rail { position: absolute; top: 10px; left: calc((100% - 60px) / 8);
  right: calc((100% - 60px) / 8); border-top: 1px solid var(--brand); }
.map-tree-rail::before, .map-tree-rail::after { content: ""; position: absolute; top: -3px;
  width: 5px; height: 5px; background: var(--brand); }
.map-tree-rail::before { left: -2px; } .map-tree-rail::after { right: -2px; }
.map-tree-cols { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
.map-tree-cols span { height: 46px; box-shadow: inset 0 0 0 1px var(--line); position: relative; }
.map-tree-cols span::before { content: ""; position: absolute; left: 50%; top: -12px;
  height: 12px; border-left: 1px solid var(--brand); }
.map-note { max-width: 68ch; }

/* ---- Member pass, rebuilt as a credential -------------------------------
   The facts carry the card, because they are what a member reads out when a
   scanner fails. The code zone is honest about waiting on the credential
   system rather than drawing a shape that pretends to be scannable. */
.pass-card { background: var(--ink); color: white; border-radius: 16px; overflow: hidden;
  display: flex; flex-direction: column; min-height: 0; padding: 20px; gap: 0; }
.pass-top { display: flex; align-items: center; justify-content: space-between;
  font-family: var(--sans); font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; }
.pass-top span { opacity: .62; }
.pass-holder { margin-top: 26px; }
.pass-holder p { margin: 0 0 4px; font-size: 12px; letter-spacing: .1em;
  text-transform: uppercase; opacity: .55; }
.pass-holder strong { display: block; font: 26px/1.05 var(--display); font-weight: 400; }

.pass-facts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
  margin: 22px 0 0; padding: 18px 0 0; border-top: 1px solid rgb(255 255 255 / .12); }
.pass-facts div { min-width: 0; }
.pass-facts dt { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; opacity: .55; }
.pass-facts dd { margin: 5px 0 0; font-family: var(--mono); font-size: 14px; line-height: 1.3; }

.pass-code { margin-top: 22px; }
.pass-code-zone { min-height: 108px; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px; text-align: center;
  padding: 16px; background: rgb(255 255 255 / .12);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / .12); }
.pass-code-zone span { font-size: 13px; font-weight: 600; letter-spacing: .06em;
  text-transform: uppercase; }
.pass-code-zone small { font-size: 12px; line-height: 1.4; opacity: .6; max-width: 30ch; }
.pass-state { display: flex; align-items: center; gap: 7px; margin: 12px 0 0;
  font-size: 12px; opacity: .7; }
.pass-state .icon { width: 13px; height: 13px; }

/* The rail is a carousel, so the next card should read as a peek rather than as
   chopped text. Cards narrow enough to show a real second item, text clamped so
   it never runs into the cut, and the edge fades instead of ending on a chop. */
.announce-rail {
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 82%, transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 82%, transparent 100%);
}
.announce { flex: 0 0 68%; }
.announce em, .announce span {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
.announce em { -webkit-line-clamp: 1; }


/* Tap targets. The audit measured 23 controls under 44px, all on Events. Where
   the drawn control is smaller than a finger, a pseudo-element carries the hit
   area so the drawing stays the size the design wants. */
.chip, .filter-row button, .event-actions button, .segments button { position: relative; }
.chip::after, .filter-row button::after, .event-actions button::after,
.segments button::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  transform: translateY(-50%); height: 44px; }
.event-card .event-title { position: relative; }
.event-card .event-title::after { content: ""; position: absolute; left: 0; right: 0;
  top: 50%; transform: translateY(-50%); height: 44px; }

/* The remaining small ones: text links and any button a later rule shrank. */
.linkish { position: relative; }
.linkish::after { content: ""; position: absolute; left: 0; right: 0; top: 50%;
  transform: translateY(-50%); height: 44px; }
.btn, .primary-action, .secondary-action { min-height: 44px; }

.renew-actions .btn, .action-row .btn { min-height: 44px; }
/* The round chrome button is drawn at 38 to sit well on the image; the hit area
   reaches 44 without changing what is drawn. */
.round-btn::after { content: ""; position: absolute; inset: -3px; }

/* Section header, after the reference: a sentence-case heading with a quiet
   link opposite it, rather than a caps eyebrow with a count. */
.rail-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: 16px; margin: 22px 0 12px; }
/* One step darker than the greeting: at 19px this is normal text by WCAG and
   --quiet is 4.28:1, under the 4.5 floor. The greeting clears it at 24px, where
   3:1 applies. Same family, the size decides the step. */
.rail-head h2 { margin: 0; font: 19px/1.15 var(--display); font-weight: 400;
  letter-spacing: -.015em; color: var(--muted); }
.rail-more .icon { width: 13px; height: 13px; opacity: .65; }
.rail-more { font-family: var(--sans); font-size: 14px; color: var(--muted);
  text-decoration: none; display: inline-flex; align-items: center; gap: 5px;
  border: 0; background: none; min-height: 44px; padding: 0 0 0 12px; cursor: pointer; }


/* A dim lobby and an older reader: nothing inside the device goes under 11px.
   The system documents 16px body and 12px small; these were the last holdouts. */
.event-copy .inline-actions button { font-size: 12px; }
.access-note { font-size: 11px; }

/* Patron card, after the reference: a portrait plate badged with the mark, the
   name in display beneath it, the role under that. There is no photograph of a
   member in this prototype, so the plate says so rather than borrowing a face. */
.patron-profile { margin: 4px 0 8px; }
.patron-portrait { position: relative; aspect-ratio: 1 / 1; border-radius: var(--r-field);
  background: var(--media-ground); overflow: hidden; display: grid; place-items: center; }
.patron-badge { position: absolute; top: 14px; left: 14px; display: inline-flex;
  align-items: center; height: 30px; padding: 0 12px; border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--off-white) 72%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%); color: var(--brand); }
.patron-badge .lockup { height: 12px; }
.patron-initials { font: 58px/1 var(--display); font-weight: 400; letter-spacing: -.03em;
  color: rgb(255 255 255 / .82); }
.patron-noimg { position: absolute; bottom: 14px; left: 14px; font-family: var(--sans);
  font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  color: rgb(255 255 255 / .60); }
.patron-name { margin: 18px 0 6px; font: 32px/1.04 var(--display); font-weight: 400;
  letter-spacing: -.025em; }
.patron-role { margin: 0; font-size: 13px; line-height: 1.5; color: var(--muted); }

/* ---- Atoms: scale and icon states ---------------------------------------
   The reference shows the size ladder before the state matrix, because a
   reader needs to know how big a control is before they care what it does
   when pressed. Sizes are the ones the product actually ships. */
.atom-group { margin: 30px 0 14px; font-family: var(--sans); font-size: 11px;
  font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--quiet); }
.atom-group:first-child { margin-top: 0; }
.btn-scale { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 28px;
  padding: 22px; background: var(--off-white); border: 1px solid var(--line);
  border-radius: var(--r-field); margin-bottom: 8px; }
.btn-scale figure { margin: 0; display: grid; gap: 10px; justify-items: start; }
.btn-scale figcaption { font-family: var(--sans); font-size: 11px; color: var(--ink);
  display: grid; gap: 2px; }
.btn-scale figcaption span { font-family: var(--mono); font-size: 10px; color: var(--quiet); }
.btn--sm { min-height: 36px; padding: 0 16px; font-size: 11px; }
.btn--lg { min-height: 50px; padding: 0 26px; font-size: 13px; }

/* The icon button as a documented control, with the same five states as the
   rest. Thin stroke on focus, matching every other control on the sheet. */
.round-btn--spec { border: 1px solid var(--line); background: var(--off-white); }
.round-btn--spec:active, .round-btn--spec.is-hover { background: var(--gray); }
.round-btn--spec:active, .round-btn--spec.is-active { background: var(--gray-pressed); }
.round-btn--spec.is-focus { outline: 1.5px solid var(--harmony); outline-offset: 2px; }
.round-btn--spec:disabled, .round-btn--spec.is-disabled { background: var(--raised);
  color: var(--quiet); border-color: var(--line); cursor: not-allowed; }

/* The portrait plate is an upload target, not a placeholder. It says what it
   wants rather than reporting an absence. */
.patron-portrait { border: 0; padding: 0; width: 100%; cursor: pointer; }
.patron-upload { position: absolute; bottom: 14px; left: 14px; display: inline-flex;
  align-items: center; gap: 7px; min-height: 36px; padding: 0 14px;
  border-radius: var(--r-pill); font-family: var(--sans); font-size: 12px;
  font-weight: 600; color: var(--ink);
  background: color-mix(in srgb, var(--off-white) 82%, transparent);
  backdrop-filter: blur(18px) saturate(160%);
  -webkit-backdrop-filter: blur(18px) saturate(160%); }
.patron-upload .icon { width: 14px; height: 14px; }

/* Season figures carry what they are counting. A 6 on its own could be shows,
   seats or months; "2 of 6" and "Orchestra A" cannot be misread. */
.stat-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; margin: 0; }
.stat-row > div { padding: 14px 0; border-bottom: 1px solid var(--line); min-width: 0; }
.stat-row dt { font-family: var(--sans); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--quiet); }
.stat-row dd { margin: 6px 0 0; font: 26px/1 var(--display); font-weight: 400;
  letter-spacing: -.02em; display: flex; align-items: baseline; gap: 6px; }
.stat-row dd span { font-family: var(--sans); font-size: 11px; font-weight: 500;
  letter-spacing: 0; color: var(--muted); }

/* ---- Announcements as a panel ------------------------------------------
   Not a card on a scrim: it slides in and owns the screen, the way a
   notifications page does. No backdrop and no blur, so nothing behind it
   competes and the list is simply the thing you are looking at. */
.stage-over[data-overlay-panel="announcements"] { background: none;
  backdrop-filter: none; -webkit-backdrop-filter: none; animation: none;
  display: block; padding: 0; }
.modal-card { position: absolute; inset: 0; display: flex; flex-direction: column;
  background: var(--off-white); border-radius: 0; box-shadow: none;
  animation: panel-in 420ms cubic-bezier(.32, .72, 0, 1) both; }
/* In from the right, the way a pushed view arrives. The curve is the long
   decelerating one, so it settles rather than snapping. */
@keyframes panel-in { from { transform: translateX(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .modal-card { animation: none; } }
.modal-head { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: calc(var(--safe-top) + 4px) 18px 14px;
  border-bottom: 1px solid var(--line); }
.modal-head h2 { margin: 0; font: 22px/1.15 var(--display); font-weight: 400;
  letter-spacing: -.02em; }
.modal-close { width: 36px; height: 36px; border: 0; border-radius: var(--r-pill);
  background: var(--gray); color: var(--ink); display: grid; place-items: center;
  cursor: pointer; flex: none; }
.modal-close .icon { width: 15px; height: 15px; }
.modal-body { flex: 1; overflow: auto; padding: 4px 18px calc(104px + var(--safe-bottom));
  scrollbar-width: none; }
.modal-body::-webkit-scrollbar { display: none; }
.modal-body .section-block { margin-top: 18px; }
.modal-body .notice-list, .modal-body .notice-item { margin-left: 0; margin-right: 0;
  width: auto; }

/* The promotional drawer is a quarter of the screen. It is an offer, not a task,
   so it announces itself and leaves. At this height the contents have to be
   compact rather than merely scrollable, or the cap just hides them. */
.sheet-panel { max-height: 25%; overflow: auto; padding: 8px 18px 16px;
  scrollbar-width: none; }
.sheet-panel::-webkit-scrollbar { display: none; }
.sheet-grip { margin: 0 auto 10px; }
.sheet-eyebrow { margin: 0 0 4px; }
.sheet-panel h2 { font-size: 18px; line-height: 1.15; margin-bottom: 4px; }
.sheet-body { margin-bottom: 10px; font-size: 12px; line-height: 1.4; }
.sheet-panel .field { margin-bottom: 0; }
.sheet-panel .field label { display: none; }
.sheet-panel .field input { min-height: 40px; }
.sheet-actions { grid-auto-flow: column; grid-auto-columns: 1fr; gap: 8px; margin-top: 10px; }
.sheet-actions .btn { min-height: 42px; }
.sheet-foot { display: none; }

/* Choosing how you get in is a decision, not a destination: it rises over the
   screen you were on and leaves you there. Taller than the promo drawer,
   because it holds two real options rather than an offer. */
.access-sheet { max-height: 62%; }
.access-sheet h2 { font: 26px/1.08 var(--display); font-weight: 400;
  letter-spacing: -.025em; margin: 0 0 14px; }
.access-sheet .choice { border-top: 1px solid var(--line); }
.access-sheet .choice:last-of-type { border-bottom: 1px solid var(--line); }
.access-sheet .access-note { margin: 14px 0 0; }

/* A membership is issued by an institution, not by the platform it is held in,
   so the stamp names the institution. No marks exist for these, and drawing an
   approximation of a real logo is worse than setting the name. */
.member-head { align-items: baseline; gap: 8px; }
.member-issuer { font-family: var(--sans); font-size: 11px; font-weight: 650;
  letter-spacing: .06em; text-transform: uppercase; color: var(--brand); }


/* ---- Included shows ------------------------------------------------------
   The same row the Events screen uses: date block on the left, the show and
   where it is on the right. One row format across the app, so a show reads the
   same wherever it is listed. Two of the six have no date yet, and the slot
   says so rather than being left blank. */
.incl-shows { display: grid; }
.incl-shows .event-card { padding: 15px 0; margin: 0;
  border-bottom: 1px solid var(--line); background: none;
  grid-template-columns: 38px 1fr; gap: 14px; }
.incl-shows .event-card:last-child { border-bottom: 0; }
.incl-shows .event-title { font-size: 16px; margin-top: 4px; }
.event-when.is-unset { padding-top: 5px; }
.event-when.is-unset span { margin-top: 0; }
.event-open { margin: 6px 0 0; font-family: var(--sans); font-size: 11px;
  letter-spacing: .06em; text-transform: uppercase; color: var(--quiet); }

/* The account row was a full-bleed band pinned above the navigation, which read
   as fixed chrome rather than the last item on the page. It comes off the bleed
   rule entirely so it sits on the same gutter as everything above it. */
/* The bleed group gives every settings-list 18px of padding so its hairline
   rows can reach the device edge. As a card it must sit on the gutter instead,
   which means zeroing the padding too — zeroing only the margin left the card
   inset 36px against everything else's 18. */
.settings-list--single { border-top: 0; margin: 10px 0 0;
  margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;
  width: 100%; }
/* Filled, like every other card on the screen. An outline was the one card
   treatment that appeared once. */
.settings-list--single button { border: 0; background: var(--gray);
  border-radius: 14px; padding: 0 16px; min-height: 58px;
  margin-left: 0; margin-right: 0; width: 100%; box-sizing: border-box; }


/* ---- The pass as the evening, not a card on a page -----------------------
   After the reference: the poster is the surface. The image runs to the device
   edges and dissolves into a dark ground that carries the billing, and the one
   white object on the screen is the thing you actually hold at the door. The
   date takes the lemon because on this ground burgundy is unreadable, and
   lemon is already in the palette. */
/* The chrome scrolls away with the poster. Sticky leaves the pill on top of the
   billing; absolute pins it to the phone, which is worse — it then rides over
   the ticket card. Relative keeps it in flow, and the stage's negative top
   margin still pulls the photograph up underneath it. */
.pass-chrome { position: relative; z-index: 20; }
.pass-stage { position: relative; margin: 0 -18px;
  background: var(--off-black); color: var(--off-white);
  padding: 0 18px 26px;
  margin-top: calc(-1 * (var(--safe-top) + 52px)); }
.pass-poster { position: absolute; top: 0; left: 0; right: 0;
  height: calc(min(874px, 100vh - 78px) * 0.29); }
.pass-poster img { display: block; width: 100%; height: 100%; object-fit: cover;
  object-position: center 42%; }
.pass-poster-veil { position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    rgb(16 16 16 / .55) 0, rgb(16 16 16 / .18) 34%,
    rgb(16 16 16 / .82) 82%, var(--off-black) 100%); }
.pass-billing { position: relative;
  padding-top: calc(min(874px, 100vh - 78px) * 0.29); }
.pass-billing h1 { margin: 16px 0 0; font: 23px/1.12 var(--display); font-weight: 400;
  letter-spacing: -.025em; color: var(--off-white); }
.pass-billing-when { margin: 8px 0 0; font-family: var(--sans); font-size: 13px;
  font-weight: 600; color: var(--accent); }
.pass-billing-where { margin: 4px 0 0; font-family: var(--sans); font-size: 13px;
  color: rgb(255 255 255 / .58); }

.pass-ticket { position: relative; margin-top: 20px; background: var(--off-white);
  color: var(--ink); border-radius: 16px; padding: 16px; display: grid; gap: 14px; }
.pass-ticket-row { display: grid; grid-template-columns: 38px 1fr; gap: 12px;
  align-items: center; }
.pass-ticket-ico { width: 38px; height: 38px; display: grid; place-items: center;
  border-radius: 10px; background: var(--pressed); color: var(--ink); }
.pass-ticket-row small { display: block; font-family: var(--sans); font-size: 11px;
  letter-spacing: .06em; text-transform: uppercase; color: var(--quiet); }
.pass-ticket-row strong { display: block; margin-top: 3px; font: 19px/1.1 var(--display);
  font-weight: 400; letter-spacing: -.02em; }
.pass-ticket-seats { display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
  padding-top: 14px; border-top: 1px solid var(--line); }
.pass-ticket-seats dt { font-family: var(--sans); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--quiet); }
.pass-ticket-seats dd { margin: 4px 0 0; font-family: var(--sans); font-size: 13px;
  font-weight: 600; line-height: 1.3; }
.pass-ticket .pass-code-zone { background: var(--gray); color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line); border-radius: 12px; min-height: 96px; }
.pass-ticket .pass-code-zone small { opacity: 1; color: var(--muted); }
.pass-ticket .btn { width: 100%; }


/* ---- Events, grouped by membership ---------------------------------------
   A month heading answers "when", which every row already says. Grouping by
   the membership answers the question the app is actually for: what does each
   relationship get me, and what falls outside all of them. */
.event-group + .event-group { margin-top: 26px; }
.event-group-head { display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; margin: 0 0 6px; }
.event-group-name { margin: 0; font: 17px/1.15 var(--display); font-weight: 400;
  letter-spacing: -.02em; color: var(--ink); }
.event-group-kind { font-family: var(--sans); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--quiet); white-space: nowrap; }


/* The three relationships stated as classifiers rather than as two lines of
   prose that repeated the membership cards. Name on the left, what kind of
   thing it is on the right, the same pairing the Events groups use. */
.patron-classifiers { list-style: none; margin: 14px 0 0; padding: 0;
  display: grid; gap: 0; }
.patron-classifiers li { display: flex; justify-content: space-between;
  align-items: baseline; gap: 12px; padding: 10px 0;
  border-bottom: 1px solid var(--line); }
.patron-classifiers li:last-child { border-bottom: 0; }
.patron-classifiers span { font-size: 14px; color: var(--ink); }
.patron-classifiers em { font-style: normal; font-family: var(--sans);
  font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--quiet); white-space: nowrap; }

/* The account's sub-blocks now carry the same head as the Events groups, so
   the two screens describe a membership the same way. */
.sub-module .event-group-head { margin-bottom: 8px; }


/* ---- No lateral drift ----------------------------------------------------
   `overflow: auto` scrolls both axes, so any child a pixel over its box let the
   screen slide sideways under a thumb and rubber-band into the page behind it.
   Each screen scrolls on one axis and keeps its overscroll to itself. */
.screen { overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; }
/* `overflow-x: hidden` here forces overflow-y to auto, which makes body a
   scroll container and silently kills every position: sticky inside it — the
   identity rail among them. `clip` clamps the same overflow without
   establishing a scrollport. */
html, body { overflow-x: clip; overscroll-behavior: none; }


/* Every horizontal scroller keeps its overscroll to itself, so a sideways flick
   inside a rail never chains out to the screen or the page behind it. */
.filter-row, .announce-rail, .member-rail, .segments {
  overscroll-behavior-x: contain; overflow-y: hidden; }
.filter-row, .announce-rail { scroll-snap-type: x proximity; }


/* Plan status is an object, not a strip of loose text, so it sits in the same
   card the memberships use. The renewal date is the one thing to act on, so it
   takes the brand hue and everything else stays neutral. */
.plan-strip { display: flex; justify-content: space-between; align-items: center;
  gap: 14px; background: var(--gray); border-radius: 14px;
  padding: 14px 16px; margin: 0; }
.plan-strip b { display: block; font: 17px/1.15 var(--display); font-weight: 400;
  letter-spacing: -.02em; }
.plan-strip small { display: block; margin-top: 3px; font-size: 12px; color: var(--muted); }
.plan-flag { flex: none; font-family: var(--sans); font-size: 10.5px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase; color: var(--brand);
  text-align: right; }

.renew-block p { font-size: 13px; line-height: 1.5; }
.renew-actions { margin-top: 12px; align-items: center; }
.renew-actions .btn--tertiary { padding: 0 4px; }

/* Controls is one object with rows inside it, not six separate cards. Filled
   once, parted by hairlines, corners only on the ends. */
.settings-list:not(.settings-list--single) { background: var(--gray);
  border-radius: 14px; border-top: 0; overflow: hidden;
  margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;
  width: 100%; }
.settings-list:not(.settings-list--single) button { padding: 0 16px;
  border-bottom: 1px solid rgb(0 0 0 / .06); margin-left: 0; margin-right: 0;
  width: 100%; box-sizing: border-box; }
.settings-list:not(.settings-list--single) button:last-child { border-bottom: 0; }

/* The dark stage runs to the device edges, so the foot fade that lifts content
   off the tab bar has nothing to fade into. The pass screen carries its own
   clearance instead: the ticket card ends above the bar rather than under it. */
.screen[data-screen="subscription"] .pass-stage { margin-bottom: 18px; }
.screen[data-screen="subscription"] { padding-bottom: 118px; }

/* The bell says there is something unread without asserting how many. A count
   on a decision surface has to be right every time; a dot only has to be true. */
.round-btn.has-unread::before { content: ""; position: absolute; top: 1px; right: 1px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--off-white);
  box-shadow: 0 0 0 2px var(--off-black); z-index: 1; }


/* ---- Happy path ----------------------------------------------------------
   Five nodes out of fifty-three. The path is drawn on top of the map rather
   than as a sixth node kind, because a node's kind says what it is and the
   step says where it falls in the one journey the app exists for. A node can
   be both, so the treatment has to stack: the lemon edge and the numbered
   badge sit over whatever color the node already carries. */
.site-node[data-step] { border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent); }
.site-node[data-step]::after { content: attr(data-step);
  position: absolute; top: -9px; left: -9px; z-index: 3;
  width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--accent); color: var(--off-black);
  font-family: var(--sans); font-size: 11px; font-weight: 700;
  font-variant-numeric: tabular-nums; }
/* The nav nodes already use ::before for their NAV slot, so the badge cannot
   collide with it: it sits left, the slot sits right. */
.node-primary[data-nav-slot][data-step]::after { top: -9px; left: -9px; }
.swatch-path { background: var(--accent); }
.legend-key.is-path { font-weight: 650; }


/* ---- Typography: the three weights that ship, and the gap ----------------
   The specimen shows what the face does; the rack below it shows what the face
   has. On a trial cut the second is the load-bearing half. */
.ft-weights { display: grid; gap: 18px; margin: 22px 0 30px; }
.ft-weights article { display: grid; gap: 8px;
  padding-bottom: 18px; border-bottom: 1px solid var(--line); }
.ft-weights article:last-child { border-bottom: 0; padding-bottom: 0; }
/* One unbroken string with a single space in it, so it needs an explicit
   break rule or it runs past the column instead of wrapping like the source. */
.ft-weights b { font-family: var(--display); font-size: clamp(18px, 2vw, 27px);
  line-height: 1.14; letter-spacing: -.015em; color: var(--ink);
  overflow-wrap: anywhere; }
.ft-weights span { font-family: var(--sans); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--quiet); }

/* Coverage is the state being reported, so it takes a state color: the two
   already in the palette rather than a green and a red invented for this rack. */


/* ---- Map node preview ----------------------------------------------------
   A live clone of the screen, scaled. It floats over the map rather than
   pushing it, and it never takes the pointer — hovering the preview must not
   count as leaving the node, or the thing would flicker itself out of
   existence. */
.map-preview { position: fixed; z-index: 90; pointer-events: none;
  border-radius: 20px; overflow: hidden;
  background: var(--surface);
  box-shadow: 0 18px 50px rgb(20 20 18 / .22), 0 0 0 1px var(--line);
  opacity: 0; transform: translateY(6px) scale(.965);
  transform-origin: center;
  /* Eased in, not snapped. 140ms on the shared curve read as an instant
     appearance because the curve is already near-linear at that length; the
     screen has to grow into place or it looks pasted. A short opacity lead so
     it is visible before it finishes settling, and a longer decelerating
     transform so it arrives rather than lands. */
  transition: opacity 180ms cubic-bezier(.25, .8, .35, 1) 20ms,
              transform 340ms cubic-bezier(.16, 1, .3, 1); }
.map-preview.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .map-preview { transition: opacity 120ms linear; transform: none; }
  .map-preview.is-in { transform: none; }
}

/* The clone is laid out at full device size and scaled from the top left, so
   the numbers in the JS stay the real ones rather than pre-divided guesses. */
.map-preview-scaler { transform-origin: 0 0; }
.map-preview-phone { width: 402px; height: 874px; min-height: 0;
  border: 0; border-radius: 0; box-shadow: none;
  background: var(--surface); position: relative; overflow: hidden; }
/* Inside the preview the screen is a still, not a scroller. */
.map-preview .screen { position: absolute; inset: 0; overflow: hidden; }
.map-preview .bottom-nav { position: absolute; }
/* The splash advances itself after two seconds; a preview that empties while
   you look at it is worse than no preview. */
.map-preview .splash-screen > img { animation: none; }


/* ---- Fill, not rules -----------------------------------------------------
   After the ElevenLabs reference: rows are filled and rounded, and nothing in
   the app is separated by a hairline. A rule and a fill do the same job, and
   running both means the separation is stated twice. The rules come off the
   app's lists; the documentation views keep theirs, because a spec sheet is a
   table and a table wants rules. */
.screen .notice-list,
.screen .held-list,
.screen .incl-list { display: grid; gap: 6px; }
.screen .notice-item,
.screen .held-list li > button {
  background: var(--raised); border: 0; border-radius: var(--r-field);
  padding: 12px 14px; margin: 0; width: 100%; box-sizing: border-box; }
.screen .notice-item { align-items: center; }
.screen .held-list li > button { grid-template-columns: 58px 1fr 16px; }
.screen .held-thumb { width: 58px; height: 58px; border-radius: 10px; }
.screen .incl-list li { border: 0; }

/* The settings rows keep their grouped card; only the internal rules go. */
.screen .settings-list:not(.settings-list--single) button { border-bottom: 0; }
.screen .settings-list:not(.settings-list--single) { display: grid; gap: 3px;
  background: none; border-radius: 0; overflow: visible; }
.screen .settings-list:not(.settings-list--single) button {
  background: var(--raised); border-radius: var(--r-field); }

/* Figures and key-value rows read as a stack of filled rows too. */
.screen .kv-list { display: grid; gap: 3px; }
.screen .kv-list > div { background: var(--raised); border: 0;
  border-radius: var(--r-field); padding: 13px 14px; margin: 0;
  width: 100%; box-sizing: border-box; }
.screen .stat-row { gap: 3px; }
.screen .stat-row > div { background: var(--raised); border: 0;
  border-radius: var(--r-field); padding: 13px 12px; }
.screen .patron-classifiers li { border: 0; background: var(--raised);
  border-radius: var(--r-field); padding: 12px 14px; margin-bottom: 3px; }

/* Chips and the segment control take the same fill as everything else. */
.chip { background: var(--raised); }
.segments { background: var(--raised); }
.segments button.is-active { background: var(--surface); }


/* ---- Frosted glass -------------------------------------------------------
   After the ElevenLabs reference: the panel itself is the glass. There is no
   dark scrim over the screen — the blur under the panel is what separates it,
   so the photograph or the list behind stays legible and the sheet reads as
   sitting on the surface rather than over a dimmed one.

   The tint is a light warm white at partial alpha with a wide blur and a
   saturation lift, which is what keeps colour behind the glass from going
   muddy. A hairline at the top edge catches the light the way a real pane
   does; without it the panel's edge disappears into what it is blurring. */
:root {
  --glass:      rgb(251 250 249 / .72);
  --glass-blur: blur(34px) saturate(180%);
  --glass-edge: inset 0 1px 0 rgb(255 255 255 / .55);
}

.sheet-scrim { background: rgb(16 16 16 / .10); backdrop-filter: blur(1px); }

.sheet-panel {
  background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-radius: 22px 22px 0 0;
  box-shadow: var(--glass-edge), 0 -12px 40px rgb(20 20 18 / .10); }
.sheet-grip { background: rgb(16 16 16 / .18); }

/* The full-screen overlays are the same material at a heavier blur, because
   they cover the whole surface and have more behind them to quiet. */
.stage-over { background: rgb(251 250 249 / .62);
  backdrop-filter: blur(44px) saturate(180%);
  -webkit-backdrop-filter: blur(44px) saturate(180%); }
.modal-head { border-bottom: 0; }
.modal-card { background: var(--glass);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-edge); }

/* Rows inside glass take a lighter fill than rows on paper, or the two
   materials stack into a solid and the glass stops reading as glass. */
.sheet-panel .choice,
.modal-card .notice-item { background: rgb(255 255 255 / .55); }

/* Safari on iOS drops backdrop-filter under memory pressure; without a solid
   floor the panel would render transparent and the text would sit on the
   photograph. */
@supports not (backdrop-filter: blur(1px)) {
  .sheet-panel, .modal-card { background: var(--surface); }
  .stage-over { background: var(--surface); }
}

/* Half the device. Three benefits and their actions fit; anything longer
   scrolls inside the sheet rather than growing it toward a full screen. */
.benefits-sheet { max-height: 52%; padding-bottom: calc(26px + var(--safe-bottom)); }
.benefits-sheet h2 { margin: 0 0 14px; font: 26px/1.08 var(--display); font-weight: 400;
  letter-spacing: -.025em; }
.benefits-sheet .benefit-list { display: grid; gap: 6px; margin: 0;
  width: 100%; padding: 0; border: 0; }
.benefits-sheet .benefit-list button { background: rgb(255 255 255 / .55);
  border: 0; border-radius: var(--r-field); padding: 13px 14px; margin: 0;
  width: 100%; box-sizing: border-box; }

/* The tab bar is chrome for the screen, not for a sheet over it. z-index 40
   put the bar's own stacking above the panel's last row. */
.sheet { z-index: 60; }








/* Inside a clone, 100vh is the browser window, not the 874px phone the clone
   is drawn at — so the hero and the pass poster resolved to the wrong height
   and the title landed under the chrome. Pin both to the device. */

/* The clone carries the app's own type; the document's color must not bleed in. */




/* Scoped to the overview's own heading. Unscoped, it matched every h2 inside
   the cloned screens — the announcements title and each section head rendered
   at 58px, the same bleed that blew up the hero earlier. */

/* The strip breaks the reading measure: at 62ch it would be two phones wide. */
/* Out of the 62ch reading measure and across the page: at the column width
   only four of the eleven screens would be reachable. */

/* A grid item's default min-width is auto, so the figure could not shrink
   below the 402px scaler inside it and every frame rendered at full device
   width regardless of the 186px track. */


/* ---- Buttons: square, three tiers ----------------------------------------
   After the supplied spec: sharp corners, and three variants that differ by
   fill rather than by shape. Filled for the ask, filled-light for the
   alternative, outlined for the one you can ignore. Each has a pressed state
   one step along, not a new color.

   The spec draws the filled tier black; the primary stays burgundy here,
   because the mark is always burgundy and a black primary next to a burgundy
   mark reads as two brands. Shape and hierarchy are the transferable part. */
.btn { border-radius: 0; letter-spacing: 0; font-weight: 650; padding: 0 24px; }

.btn--primary { background: var(--brand); color: var(--off-white); }
.btn--primary:active, .btn--primary.is-hover { background: var(--brand-raised); }
.btn--primary:active, .btn--primary.is-active { background: var(--brand-pressed); }

.btn--secondary { background: var(--surface); color: var(--ink); }
.btn--secondary:active, .btn--secondary.is-hover { background: var(--raised); }
.btn--secondary:active, .btn--secondary.is-active { background: var(--pressed); }

/* The outline is the button, so it does not gain a fill on hover — it deepens.
   A tertiary that fills on hover reads as a secondary that was hiding. */
.btn--tertiary { background: transparent; color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line); text-decoration: none; padding: 0 24px; }
.btn--tertiary:active, .btn--tertiary.is-hover { background: transparent;
  box-shadow: inset 0 0 0 1px var(--quiet); }
.btn--tertiary:active, .btn--tertiary.is-active { background: var(--pressed);
  box-shadow: inset 0 0 0 1px var(--quiet); }

/* Everything that was a pill because .btn is a pill follows the same corner. */
.hero-cta, .pass-ticket .btn, .renew-actions .btn,
.sheet-actions .btn, .primary-action, .secondary-action { border-radius: 0; }


/* ---- Identity system: legibility -----------------------------------------
   The document was set at 11-13px, which is a specification sheet's size, not
   a reading size. Everything a person actually reads moves up; the numbers and
   token names stay small because they are looked up rather than read. */
.identity-section { grid-template-columns: 200px 1fr; gap: 36px; padding: 54px 0; }
.identity-section > header { grid-template-columns: 34px 1fr; }
.identity-section > header span { font-family: var(--sans); font-size: 13px;
  font-weight: 650; color: var(--quiet); }
.identity-section > header h2 { font-family: var(--sans); font-size: 16px;
  font-weight: 650; letter-spacing: -.01em; color: var(--ink); }

.principle-list { column-gap: 40px; row-gap: 8px; }
.principle-list article { padding: 18px 0; }
.principle-list b { font-family: var(--sans); font-size: 15px; font-weight: 650;
  letter-spacing: -.005em; color: var(--ink); }
.principle-list p { margin: 8px 0 0; font-size: 14px; line-height: 1.6;
  color: var(--muted); }

.token-note { font-size: 14px; line-height: 1.6; margin: 24px 0 0; }
.access-rules article { padding: 18px 0; }
.access-rules b { font-family: var(--sans); font-size: 15px; font-weight: 650;
  letter-spacing: -.005em; color: var(--ink); }
.access-rules p { margin: 8px 0 0; font-size: 14px; line-height: 1.6; color: var(--muted); }

.ratio-rack { gap: 6px; }
.ratio-rack article { padding: 14px 15px; border-radius: var(--r-field); gap: 5px; }
.ratio-rack b { font-family: var(--sans); font-size: 17px; font-weight: 650; }
.ratio-rack span { font-size: 12.5px; line-height: 1.45; }

.identity-intro strong { font-size: 22px; line-height: 1.25; letter-spacing: -.015em; }
.identity-intro span { font-size: 15px; line-height: 1.55; }
.identity-subnav a { font-size: 14px; }
.identity-subnav > span { font-size: 12px; }
.photo-lede { font-size: 16px; line-height: 1.6; }

/* Nothing in the document sits below 12px. The values and token names were at
   9-11, which is a size for glancing at, and these are read. */
.identity-view p, .identity-view span, .identity-view small,
.identity-view i, .identity-view figcaption, .identity-view li { font-size: 13px; }
.identity-view code, .identity-view .mono-inline { font-size: 12px; }
.color-rack article { font-size: 12px; }
.color-rack code { font-size: 12px; }
.gray-rack code, .tok-rack code, .tok-rack b, .comp-rack code, .map-plate code,
.btn-scale figcaption span, .spacing-rack code, .shape-rack code { font-size: 12px; }
.identity-intro > p { font-size: 13px; letter-spacing: .06em; }
.photo-specimens figcaption, .photo-canon figcaption { font-size: 12.5px; }


/* The five questions route to the screen that answers them. A list that names
   the answer and cannot reach it is a table of contents with no page numbers.

   The li was a grid with a 30px number column, so the button landed in that
   column and every question wrapped one word per line. The li is a plain block
   now and the button carries the grid, number included. */

/* ---- About view, restored ------------------------------------------- */
/* ---- About: one page, centered ---------------------------------------- */
/* ---- About: anchored in screens ------------------------------------------
   A claim about a screen sits next to that screen. The figure is a live clone
   scaled to the column, so the page cannot describe a build that has moved on.
   It breaks out of the reading measure, because a phone at 62ch would be
   postage-stamp sized, and sits on the page ground rather than on a plate. */
/* ---- About: the overview strip -------------------------------------------
   After the deck page: every screen in one row, each named underneath, so the
   whole surface can be taken in before the prose explains any of it. The row
   scrolls rather than wrapping — a filmstrip that reflows into a grid stops
   reading as a sequence. */
/* ---- About, in the ElevenLabs register -----------------------------------
   Warm ground, sentence case, no rules, everything that groups sits on a
   rounded fill. The old page was a document with tracked uppercase labels and
   hairline lists; that reads as a spec sheet. This reads as the product's own
   surface, which is what a page introducing the product should do. */
.about-view { padding: 70px var(--page-gutter) 96px;
  max-width: none; margin: 0; }
.about-page { max-width: 62ch; margin: 0 auto; }
.about-eyebrow { margin: 0 0 10px; font-family: var(--sans); font-size: 10px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--quiet); }
.about-kicker { margin: 0 0 8px; font-family: var(--sans); font-size: 10px;
  letter-spacing: .12em; text-transform: uppercase; color: var(--quiet); }
.about-page > h1 { margin: 0 0 30px; font: 58px/.94 var(--display); letter-spacing: -.03em; }
.about-page, .about-page > p, .about-page > h1, .about-page > h2, .about-page > h3,
.about-spine li { color: var(--muted); }
.about-page > p { margin: 0 0 20px; font-size: 17px; line-height: 1.6; }
.about-spine { margin: 4px 0 24px; padding: 0; list-style: none; counter-reset: q;
  display: grid; gap: 0; }
.about-spine li { counter-increment: q; margin: 0;
  display: grid; grid-template-columns: 32px 1fr; gap: 4px; align-items: baseline;
  padding: 11px 0; border-bottom: 1px solid var(--line);
  font-size: 17px; line-height: 1.4; }
.about-spine li:first-child { border-top: 1px solid var(--line); }
.about-spine li::before { content: counter(q, decimal-leading-zero);
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .08em; color: var(--quiet); }
.about-page > h3 { margin: 30px 0 10px; font-family: var(--sans); font-size: 11px;
  font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--quiet); }
.about-page > h2 { margin: 42px 0 14px; font: 22px/1.15 var(--display); font-weight: 400;
  letter-spacing: -.02em; }
.about-owners { margin: 18px 0 22px; border-top: 1px solid var(--line); }
.about-owners > div { display: flex; justify-content: space-between; align-items: baseline;
  gap: 18px; padding: 11px 0; border-bottom: 1px solid var(--line); }
.about-owners dt { font-size: 13px; }
.about-owners dd { margin: 0; flex: none; font-family: var(--mono); font-size: 11px;
  color: var(--muted); font-variant-numeric: tabular-nums; }
.about-page > p.about-foot { margin-top: 34px; font-size: 13px; line-height: 1.6; color: var(--quiet); }
.about-shot { margin: 30px 0 34px; width: 260px; max-width: 100%; }
.about-shot-frame { position: relative; overflow: hidden;
  border-radius: 22px; background: var(--surface);
  box-shadow: 0 14px 40px rgb(20 20 18 / .12), 0 0 0 1px rgb(20 20 18 / .06); }
.about-shot-scaler { transform-origin: 0 0; }
.about-shot-phone { width: 402px; height: 874px; min-height: 0;
  border: 0; border-radius: 0; box-shadow: none;
  background: var(--surface); position: relative; overflow: hidden; }
.about-shot .screen { position: absolute; inset: 0; overflow: hidden; }
.about-shot .bottom-nav { position: absolute; }
.about-shot .splash-screen > img { animation: none; }
.about-shot .hero, .map-preview .hero { height: calc(874px * 0.40); min-height: 0; }
.about-shot .pass-poster, .map-preview .pass-poster { height: calc(874px * 0.29); }
.about-shot .pass-billing, .map-preview .pass-billing { padding-top: calc(874px * 0.29); }
.about-shot .stage, .map-preview .stage { min-height: 0; }
.about-shot-frame { color: var(--ink); }
.about-overview { margin: 0 0 46px; }
.about-overview > header { margin: 0 0 26px; }
.about-overview > header h2 { margin: 0; font: 58px/.94 var(--display);
  font-weight: 400; letter-spacing: -.03em; color: var(--ink); }
.about-overview > header p { margin: 6px 0 0; font-size: 20px; line-height: 1.3;
  color: var(--quiet); }
.about-shot, .strip .about-shot-frame { min-width: 0; }
.about-shot { margin: 0; width: 100%; }
.about-view { background: var(--canvas); }
.about-page { max-width: 68ch; }
.about-page > h1 { font-family: var(--sans); font-size: 42px; line-height: 1.02;
  font-weight: 650; letter-spacing: -.03em; color: var(--ink); margin: 0 0 34px; }
.about-overview > header h2 { font-family: var(--sans); font-size: 42px;
  line-height: 1.02; font-weight: 650; letter-spacing: -.03em; }
.about-overview > header p { font-size: 18px; letter-spacing: -.01em; }
.about-page > h2 { font-family: var(--sans); font-size: 24px; line-height: 1.15;
  font-weight: 650; letter-spacing: -.02em; color: var(--ink);
  margin: 52px 0 14px; }
.about-page > h3 { font-family: var(--sans); font-size: 15px; line-height: 1.3;
  font-weight: 650; letter-spacing: -.005em; text-transform: none;
  color: var(--ink); margin: 30px 0 8px; }
.about-page > p { font-size: 16px; line-height: 1.62; color: var(--muted); }
.about-spine { display: grid; gap: 4px; margin: 8px 0 26px; }
.about-spine li { border: 0; background: var(--surface); border-radius: var(--r-field);
  padding: 15px 16px; grid-template-columns: 30px 1fr; gap: 6px;
  font-size: 16px; line-height: 1.4; color: var(--ink); }
.about-spine li:first-child { border-top: 0; }
.about-spine li::before { color: var(--quiet); font-weight: 700; }
.about-shot-frame { border-radius: 20px;
  box-shadow: 0 10px 30px rgb(20 20 18 / .10); }
.about-spine li { display: block; padding: 0; background: none; border: 0; }
.about-spine li::before { content: none; }
.about-spine .n { font-family: var(--sans); font-size: 12px; font-weight: 700;
  letter-spacing: .06em; color: var(--quiet); font-variant-numeric: tabular-nums; }
.about-spine li button { display: grid; width: 100%;
  grid-template-columns: 26px minmax(0, 1fr) auto 16px; gap: 14px;
  align-items: center; text-align: left; border: 0; cursor: pointer;
  background: var(--surface); border-radius: var(--r-field);
  padding: 15px 16px; font: inherit; color: var(--ink);
  transition: background-color 160ms ease; }
.about-spine li button:hover { background: var(--raised); }
.about-spine li button:focus-visible { outline: 3px solid var(--brand); outline-offset: 3px; }
.about-spine .q { font-size: 16px; line-height: 1.4; }
.about-spine .to { font-family: var(--sans); font-size: 12.5px; color: var(--quiet);
  white-space: nowrap; }
.about-spine .icon-chevron { color: var(--quiet); }
.strip-scroll { width: 100vw; margin-left: calc(50% - 50vw); margin-right: calc(50% - 50vw);
  padding: 4px var(--page-gutter) 10px;
  overflow-x: auto; overscroll-behavior-x: contain;
  scrollbar-width: none;
  mask-image: linear-gradient(to right, transparent 0, #000 var(--page-gutter),
    #000 calc(100% - var(--page-gutter)), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--page-gutter),
    #000 calc(100% - var(--page-gutter)), transparent 100%); }
.strip-scroll::-webkit-scrollbar { display: none; }
.strip { display: flex; gap: 18px; margin: 0; padding: 0; list-style: none;
  width: max-content; }
.strip > li { flex: none; width: 186px; display: grid; gap: 12px; }
.strip figcaption { font-family: var(--sans); font-size: 12px;
  letter-spacing: .01em; color: var(--muted); text-align: center; }
.strip > li, .strip .about-shot, .strip .about-shot-frame { min-width: 0; }
.strip .about-shot { margin: 0; width: 100%; }
.strip figcaption { font-size: 12.5px; color: var(--quiet); letter-spacing: -.005em; }
.strip > li { width: 196px; gap: 14px; }
.strip { gap: 20px; }
.glyph-gap { margin-top: 26px; display: grid; gap: 14px; }
.glyph-proof { font-size: 13px; line-height: 1.6; color: var(--muted); }
.glyph-proof b { font-family: var(--display); font-size: 15px; color: var(--ink); }
.glyph-proof { font-size: 14px; }


/* Five tabs with their glyphs. The label-only pill was tuned for four short
   words; at five the labels crowded and the fifth wrapped onto a second line.
   Icon over label, five equal tracks, and the bar spans the device rather than
   sizing to its longest label. The icon carries the recognition, so the label
   can drop to a caption size without the tab becoming unreadable. */
/* left:50% + translateX with a percentage width measured 8px off centre.
   Explicit insets centre it exactly and cannot drift. */
.bottom-nav { display: grid; grid-template-columns: repeat(5, 1fr);
  left: 12px; right: 12px; width: auto; max-width: none; transform: none;
  gap: 2px; padding: 7px 15px; }
/* The bar is a full pill, so its own curve travels inward at the height of the
   active tab: with a 62px bar the effective radius is 31, and 7px down from
   the top the edge has already moved 11.4px in. At 6px of padding that curve
   sliced the corner off the active pill. 13px clears it. */
.bottom-nav button { flex: none; min-width: 0; min-height: 48px;
  flex-direction: column; gap: 5px; padding: 0;
  font-size: 10px; font-weight: 500; letter-spacing: -.012em;
  /* No overflow clamp here. The advance width fits the track, but a glyph's
     ink runs slightly past its advance, so `overflow: hidden` sliced the last
     letter off Subscription and Membership while every measurement reported
     them fitting. If a label ever does not fit, that is a naming problem to
     solve, not something to hide behind an ellipsis. */
  white-space: nowrap; }
.bottom-nav button .icon { display: block; width: 19px; height: 19px;
  stroke-width: 1.7; }
.bottom-nav button.is-active .icon { stroke-width: 2; }

/* Member benefits are rows in the app now, so they take the app's row fill
   rather than the hairline list they had as a standalone screen. */
/* Same trap as the settings list: the bleed group gives every one of these
   18px of padding so its hairline rows can reach the device edge. Zeroing only
   the margin left the rows inset 36px against the 18px everything else sits
   on, so they read as a narrower column inside the page. */
.screen .benefit-list { display: grid; gap: 6px; border-top: 0;
  margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0;
  width: 100%; box-sizing: border-box; }
.screen .benefit-list button { background: var(--raised); border: 0;
  border-radius: var(--r-field); padding: 14px 16px; margin: 0;
  width: 100%; box-sizing: border-box;
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 14px;
  align-items: center; }
.screen .benefit-list button > span:first-child { display: grid; gap: 3px; min-width: 0; }
.screen .benefit-list strong { font-family: var(--sans); font-size: 15px;
  font-weight: 650; letter-spacing: -.01em; line-height: 1.25; }
.screen .benefit-list small { font-size: 12.5px; color: var(--muted); }
/* The action is a chip, so it keeps its own width rather than stretching. */
.screen .benefit-list button > span:last-child { flex: none; justify-self: end;
  padding: 7px 14px; border-radius: var(--r-pill); background: var(--pressed);
  font-family: var(--sans); font-size: 12.5px; font-weight: 650;
  color: var(--ink); white-space: nowrap; }

/* Five columns is a narrower node, so the NOT BUILT badge no longer clears a
   long title on one line. The title reserves the badge's width. */
.site-node[data-kind] .node-t { padding-right: 82px; }
.site-node[data-kind]::before { max-width: 76px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }


/* ---- Seat detail ---------------------------------------------------------
   After the supplied card: the seat stated as three fields, the view from it,
   then what it cost broken out. Dark, because it is the object you hold rather
   than a page you read, and the photograph needs a ground that does not
   compete with it.

   Two blocks, two greys: the seat and its view sit on the lighter one, the
   money on near-black. The change of ground is what separates them; there is
   no rule between. */
.seat-sheet { max-height: 82%; padding-bottom: calc(22px + var(--safe-bottom)); }
.seat-card { border-radius: 16px; overflow: hidden; background: oklch(0.2600 0 0); }

.seat-head { display: grid; grid-template-columns: 1fr auto auto; gap: 18px;
  padding: 18px 18px 14px; }
.seat-field { display: grid; gap: 6px; min-width: 0; }
.seat-field.is-num { justify-items: end; min-width: 34px; }
.seat-field em { font-style: normal; font-family: var(--sans); font-size: 11px;
  font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  color: rgb(255 255 255 / .52); }
.seat-field b { font-family: var(--sans); font-size: 17px; font-weight: 650;
  letter-spacing: -.01em; color: var(--off-white); white-space: nowrap; }

.seat-view { margin: 0 18px 18px; }
.seat-view img { display: block; width: 100%; aspect-ratio: 4 / 3;
  object-fit: cover; background: var(--media-ground); }

.seat-price { margin: 0; padding: 20px 18px 22px; background: var(--off-black);
  display: grid; gap: 10px; }
.seat-price > div { display: flex; justify-content: space-between;
  align-items: baseline; gap: 18px; }
.seat-price dt { font-family: var(--sans); font-size: 15px; font-weight: 650;
  color: var(--off-white); }
.seat-price dd { margin: 0; font-family: var(--sans); font-size: 15px;
  font-weight: 650; color: var(--off-white);
  font-variant-numeric: tabular-nums; }
/* The total is the line you look for, so it gets the space rather than a rule
   or a heavier weight; everything here is already bold. */
.seat-price .is-total { margin-top: 6px; }

.seat-note { margin: 14px 2px 0; font-size: 12.5px; line-height: 1.5;
  color: var(--muted); }

/* The seats row on the ticket became the way in, so it has to read pressable. */
.pass-ticket-seats { width: 100%; border: 0; background: none; cursor: pointer;
  display: grid; grid-template-columns: 1fr 1fr 16px; gap: 14px;
  align-items: center; text-align: left; padding: 14px 0 0;
  border-top: 1px solid var(--line); }
.pass-ticket-seats span { display: grid; gap: 4px; min-width: 0; }
.pass-ticket-seats em { font-style: normal; font-family: var(--sans);
  font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--quiet); }
.pass-ticket-seats b { font-family: var(--sans); font-size: 13px;
  font-weight: 600; color: var(--ink); }
.pass-ticket-seats .icon-chevron { color: var(--quiet); }


/* ---- Marketing site, mobile ---------------------------------------------
   The public page, built to the supplied mobile design. It is the one surface
   in the prototype that is not the member app, so it keeps the marketing
   register: black ground, a heavier display setting, square edges, and no
   burgundy. Burgundy is the member app's action colour; spending it here would
   make the two read as one product when they are a site and an app.

   It carries no tab bar, because nobody signed in is looking at it. */
.mkt-screen { padding: 0 0 var(--safe-bottom); background: var(--off-black);
  color: var(--off-white); }

.mkt-bar { display: grid; grid-template-columns: 1fr auto auto; gap: 12px;
  align-items: center; padding: var(--safe-top) 18px 14px;
  border-bottom: 1px solid rgb(255 255 255 / .14); }
.mkt-mark .lockup { height: 15px; color: var(--off-white); }
.mkt-login { min-height: 34px; padding: 0 14px; border-radius: 0;
  border: 1px solid rgb(255 255 255 / .55); background: none;
  color: var(--off-white); font-family: var(--sans); font-size: 12px;
  font-weight: 600; cursor: pointer; }
.mkt-menu { width: 30px; height: 30px; border: 0; background: none;
  display: grid; align-content: center; gap: 6px; padding: 0 4px; cursor: pointer; }
.mkt-menu span { display: block; height: 1.5px; background: var(--off-white); }

.mkt-hero { padding: 34px 18px 30px; }
.mkt-hero h1 { margin: 0; font-family: var(--sans); font-size: 34px;
  line-height: 1.06; font-weight: 700; letter-spacing: -.03em; }
.mkt-hero p { margin: 18px 0 0; font-size: 14px; line-height: 1.55;
  color: rgb(255 255 255 / .70); }
.mkt-cta { margin-top: 26px; min-height: 44px; padding: 0 20px; border: 0;
  border-radius: 0; background: var(--off-white); color: var(--off-black);
  font-family: var(--sans); font-size: 13px; font-weight: 650; cursor: pointer; }
.mkt-cta.is-dark { background: var(--off-black); color: var(--off-white); }

/* The notch in the supplied art is a step out of the top-left corner. Drawn as
   a clip path so it scales with the image rather than being baked into it. */
.mkt-shot { margin: 0 0 0 18px; }
.mkt-shot img { display: block; width: 100%; aspect-ratio: 1 / 1;
  object-fit: cover;
  clip-path: polygon(30% 0, 100% 0, 100% 100%, 0 100%, 0 22%, 30% 22%); }

.mkt-clients { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding: 34px 18px 40px; }
.mkt-clients span { font-family: var(--sans); font-size: 11px; font-weight: 650;
  line-height: 1.25; letter-spacing: .01em; color: rgb(255 255 255 / .34);
  max-width: 10ch; }

/* The page turns over to the light ground here, which is the site's one
   structural move: everything above is the pitch, everything below is proof. */
.mkt-platform { background: var(--canvas); color: var(--ink);
  padding: 44px 18px 36px; }
.mkt-platform h2 { margin: 0; font-family: var(--sans); font-size: 30px;
  line-height: 1.08; font-weight: 700; letter-spacing: -.03em; }
.mkt-platform p { margin: 24px 0 0; font-size: 14px; line-height: 1.6;
  color: var(--muted); }

.mkt-facts { margin: 0; padding: 0 18px 40px; list-style: none;
  background: var(--canvas); color: var(--ink); }
.mkt-facts li { padding: 18px 0; font-family: var(--sans); font-size: 15px;
  font-weight: 650; letter-spacing: -.005em;
  border-top: 1px solid var(--line); }


/* ---- Navigation glyphs --------------------------------------------------
   The bar was carrying three signals for one state — a translucent pill, a
   heavier label, and a thicker stroke — and no single one of them was strong.
   A 1.7px Feather outline also goes soft at 19px on a dark ground, so the tag
   and the gift read as smudges before they read as signs.

   One signal now: the glyph fills. Outline and solid are the same Material
   Symbols Rounded drawing, so nothing shifts when a tab activates, and the
   label weight is constant in both states — bolding it moved every neighbour
   off centre. The pill is gone; the fill is the state.

   Metaphors, too: a price tag is not a subscription and a gift is not a
   membership. Card for the thing that renews, medal for the thing you hold.
   The map nodes carry the same two glyphs so the IA and the bar agree. */
.bottom-nav button .icon { fill: currentColor; stroke: none;
  width: 21px; height: 21px; display: block; }
.bottom-nav button .nav-solid { display: none; }
.bottom-nav button.is-active .nav-line { display: none; }
.bottom-nav button.is-active .nav-solid { display: block; }
.bottom-nav button { color: color-mix(in oklch, var(--off-white) 58%, transparent);
  font-weight: 500; }
.bottom-nav button.is-active { background: none; color: var(--off-white);
  font-weight: 500; }
.bottom-nav button:active:not(.is-active) {
  color: color-mix(in oklch, var(--off-white) 82%, transparent); }
/* One label size across every width. At 14px the words outweighed the glyphs and
   the bar read as a row of text with decoration above it; the icon now carries
   the recognition, so the label can sit at caption size. */
.bottom-nav button { font-size: 10.5px; letter-spacing: -.012em; gap: 5px; }
/* Measured, not guessed: at 11px in a 15px-padded bar, Subscription and
   Membership closed to 3.2px of each other. Half a step down on the label and
   8px of bar padding opens that to 8px, which is the smallest gap that still
   reads as two separate tabs. The labels themselves stay as the client named
   them — a rename is an IA decision, not a layout fix. */
.bottom-nav { padding-left: 8px; padding-right: 8px; }

/* ---- Navigation, UNIQLO register ----------------------------------------
   Reference: UNIQLO iOS, bottom tab bar. The bar there is not an object
   floating over the content — it is the bottom edge of the screen. Full bleed,
   square, white, one hairline holding it off the page, and no shadow, blur or
   tint to lift it. That removes every device the old capsule was using, so the
   glyph has to carry the whole signal on its own: solid for the destination
   you are on, outline for the four you are not.

   UNIQLO marks the active tab in black and keeps red for the logo and the sale
   price. Here the active mark is the brand burgundy, which is a near-black with
   warmth, so the rule is the same rule: one hue, spent once, never decorative.
   The bar loses the last rounded object in the build; the phone's own corner
   radius is the only curve left at the bottom of the screen, which is what
   UNIQLO does too. */
.bottom-nav {
  left: 0; right: 0; bottom: 0;
  width: auto; max-width: none; transform: none;
  min-height: 0;
  border-radius: 0;
  background: var(--off-white);
  border-top: 1px solid var(--line);
  box-shadow: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  padding: 9px 4px calc(9px + env(safe-area-inset-bottom, 0px));
}
.bottom-nav button { color: var(--quiet); }
.bottom-nav button.is-active { color: var(--brand); background: none; }
/* The bar no longer floats, so content stops at its edge instead of clearing a
   gap under it. */
.screen { padding-bottom: 84px; }


/* ═══════════════════════════════════════════════════════════════════════════
   UNIQLO REGISTER
   Reference: UNIQLO iOS on Mobbin. Everything below is one block on purpose —
   delete from this banner to the end of the file and the app returns to the
   register it had before.

   What their system actually is, as opposed to what it looks like:

   · White ground, and fill is not how anything is grouped. Rows are separated
     by hairlines, not by sitting in grey capsules. Grey means disabled, never
     "this is a row."
   · Square. Chips, cards, fields, thumbnails, size selectors — all zero radius.
     The one exception is the action itself: ADD TO CART is a full pill, and
     that contrast is the whole point. A rounded object means "press me."
   · Actions are uppercase and letter-spaced. It is the only place they raise
     their voice.
   · Selection is a fill, not a tint. An unselected size chip is a hairline box;
     the selected one is solid black. There is no in-between state.
   · Colour is rationed. Black does the work; their red is the logo and the sale
     price and nothing else. Here the ration goes to burgundy. Maestro red is
     not UNIQLO red and does not become it.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --r-field: 0;
  --r-chip:  0;
  --r-card:  0;
}

/* ---- Announcements ------------------------------------------------------
   It was a frosted sheet over the hero photograph, so the panel took its
   colour from whatever was behind it: the two Tonight rows and the Coming up
   row rendered as three different greys, none of them chosen, and the eyebrow
   sat grey-on-grey. Below the third row, two thirds of the panel was gradient.

   Solid white, and the rows stop being objects. A hairline between them is the
   whole grouping device, which is also what lets the empty space below the last
   one read as an ordinary end of page rather than as a hole in a card. */
.stage-over[data-overlay-panel="announcements"] {
  background: var(--off-white);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.modal-card {
  background: var(--off-white);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
  border-radius: 0;
}
.modal-head {
  padding: 14px 6px 14px 18px;
  border-bottom: 1px solid var(--line);
}
.modal-head h2 {
  font: 600 19px/1.2 var(--sans);
  letter-spacing: -.012em;
  color: var(--ink);
}
.modal-close {
  width: 44px; height: 44px;
  border-radius: 0;
  background: none;
  color: var(--ink);
}
.modal-close .icon { width: 17px; height: 17px; }
.modal-body { padding: 0 0 32px; }
.modal-body .section-block { margin-top: 0; }
/* The eyebrow is a band label, so it sits on the same 18px gutter as the rows
   it introduces and takes its air from padding rather than a margin that would
   collapse against the hairline above it. */
.modal-body .section-heading {
  margin: 0;
  padding: 22px 18px 9px;
  align-items: baseline;
}
.modal-body .section-heading h2 { color: var(--ink); }
.modal-card .notice-item {
  background: none;
  border-radius: 0;
  border-bottom: 1px solid var(--line);
  padding: 15px 18px;
  align-items: start;
  column-gap: 12px;
}
.modal-card .notice-item:last-child { border-bottom: 1px solid var(--line); }
.modal-card .notice-item:not(.is-static):active { background: var(--gray); }
/* The icon had a filled chip of its own — a third container inside a row inside
   a sheet. It is a glyph beside a heading; it does not need a box. */
.modal-card .notice-ico {
  background: none;
  border-radius: 0;
  width: 20px; height: 20px;
  align-self: start;
  margin-top: 1px;
  color: var(--quiet);
}
.modal-card .notice-ico .icon { width: 17px; height: 17px; }
.modal-card .notice-copy b { font-size: 15px; font-weight: 650; letter-spacing: -.01em; }
.modal-card .notice-item .icon-chevron { align-self: start; margin-top: 3px; }

/* ---- Selection is a fill --------------------------------------------------
   Filter chips and the segmented control were rounded tints on a rounded tray.
   UNIQLO's size selector is the model: a hairline box, and the chosen one is
   solid. Nothing in between, and no tray. */
.chip, .filter-row button {
  border-radius: 0;
  background: none;
  border: 1px solid var(--hairline);
  color: var(--ink);
}
.chip.is-on, .filter-row button.is-on {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--off-white);
}
.segments {
  background: none;
  border-radius: 0;
  padding: 0;
  gap: 0;
}
.segments button {
  border-radius: 0;
  border: 1px solid var(--hairline);
  margin-left: -1px;
  color: var(--muted);
}
.segments button:first-child { margin-left: 0; }
.segments button.is-active {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--off-white);
  font-weight: 600;
  position: relative;
  z-index: 1;
}

/* ---- The action raises its voice ------------------------------------------
   Uppercase and letter-spaced, and it stays a pill while everything around it
   goes square. Secondary is the same shape drawn in a hairline, which is how
   their ADDED TO WISH LIST sits beside ADD TO CART. */
.primary-action, .secondary-action,
.event-actions button, .mkt-cta {
  text-transform: uppercase;
  letter-spacing: .085em;
  font-size: 11px;
  font-weight: 650;
}
.secondary-action {
  background: none;
  border: 1px solid var(--ink);
  color: var(--ink);
}

/* ---- Fill is not a grouping device ---------------------------------------
   Every one of these already carried a hairline border; the grey inside it was
   doing the same job twice. On white, the hairline is the only edge and the
   rows stop reading as tiles. */
.action-row, .compact-grid button, .support-note,
.renew-block, .stat-row > div, .event-card {
  background: none;
}
.choice-icon { background: none; border: 1px solid var(--hairline); }

/* ---- Lists are hairlines, not tiles ---------------------------------------
   An earlier pass reversed this on the grounds that "a rule and a fill do the
   same job, and running both means the separation is stated twice." True, and
   UNIQLO settles which one survives: the rule. Their product lists, coupon
   lists and membership menu are all white rows over a hairline, and the fill is
   reserved for a chosen state. Rows go full bleed to the device edge with the
   content held on the 18px gutter, which is the same figure the announcements
   sheet uses, so the two read as one list system. */
.screen .notice-list,
.screen .held-list,
.screen .incl-list { gap: 0; }
.screen .notice-item,
.screen .held-list li > button {
  background: none;
  border-radius: 0;
  border-bottom: 1px solid var(--line);
  padding: 14px 18px;
}
.screen .held-list li:last-child > button,
.screen .notice-item:last-child { border-bottom: 1px solid var(--line); }
.screen .notice-item:not(.is-static):active,
.screen .held-list li > button:active { background: var(--gray); }
/* A square crop, like every product thumbnail in the reference. */
.screen .held-thumb { border-radius: 0; }

/* ---- The last of the radii ------------------------------------------------
   These carry literal values rather than the tokens, so squaring the tokens
   left them behind. */
.pass-ticket, .pass-ticket-ico, .pass-code-zone,
.plan-strip, .renew-block, .held-thumb { border-radius: 0; }
.plan-strip, .renew-block, .pass-code-zone { background: none; }
.plan-strip, .renew-block { border: 1px solid var(--hairline); }

/* ---- Fields are drawn, not filled -----------------------------------------
   A grey field says disabled in this system. An editable one is white inside a
   hairline; the disabled state keeps the fill, which is the only job it has. */
.field input {
  background: var(--off-white);
  border: 1px solid var(--hairline);
  border-radius: 0;
}
.field input:focus-visible, .field input.is-hover { background: var(--off-white); border-color: var(--quiet); }
.field input:disabled, .field input.is-disabled { background: var(--raised); border-color: transparent; }

/* ---- Every action speaks the same way -------------------------------------
   .btn is the documented component; .primary-action and .event-actions are the
   app's own. All three take the uppercase, and secondary is drawn rather than
   filled. */
.btn { text-transform: uppercase; letter-spacing: .085em; font-weight: 650; }
.btn--secondary { background: none; border: 1px solid var(--ink); }
.btn--secondary:active, .btn--secondary.is-hover { background: var(--gray); }
.benefit-list button > span:last-child {
  background: none;
  border: 1px solid var(--hairline);
  text-transform: uppercase;
  letter-spacing: .07em;
  font-size: 10px;
  font-weight: 650;
  padding: 7px 12px;
}
/* Three actions on one line, and uppercase costs width. The tracking comes back
   to .06em and the labels stop wrapping. */
.event-actions button { letter-spacing: .06em; }

/* ---- The rows the last pass could not reach -------------------------------
   These four carry `.screen`-prefixed rules, so a bare class selector loses to
   them however late it sits in the file. Same treatment, matching weight. */
.screen .kv-list > div,
.screen .patron-classifiers li,
.screen .benefit-list button {
  background: none;
  border-radius: 0;
  border-bottom: 1px solid var(--line);
}
.screen .kv-list > div:last-child,
.screen .patron-classifiers li:last-child,
.screen .benefit-list li:last-child button { border-bottom: 1px solid var(--line); }
.screen .benefit-list button:active,
.screen .patron-classifiers li:active { background: var(--gray); }
/* The scan panel is the one thing on the pass that is genuinely inert — it is
   a placeholder for a credential rendered at the door — so it keeps a fill.
   It just stops being a rounded tile inside a square card. */
.pass-ticket .pass-code-zone { border-radius: 0; }
/* Same `.screen` weight problem as the three above. The season figures are three
   numbers over a rule, which is how the reference sets a stat row. */
.screen .stat-row > div {
  background: none;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
}

/* ---------------------------------------------------------------------------
   Season journey — record, not rail.                            2026-09-09

   The strip above (six columns, month over day) is superseded. It said when
   but never where, and the earlier note that show names "were the same six
   rows twice" no longer holds: the rows now carry the room, which the Included
   shows list does not, and the room is what picks the photograph.

   Rows are white over a hairline, per the fill-is-not-a-grouping-device rule.
   The current stop is marked by a burgundy rail, not a filled block — one hue,
   one signal, text stays near-black.

   These selectors carry .screen because four earlier rules in this file lost
   silently to `.screen .x` and looked applied when they were not. */
.screen .journey { display: block; grid-template-columns: none; gap: 0;
  margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--hair); }
.screen .journey .journey-entry { display: grid; align-items: center;
  grid-template-columns: 92px 1fr; gap: 12px;
  padding: 10px 12px 10px 0; border-radius: 0; justify-items: stretch;
  border-bottom: 1px solid var(--hair); border-left: 2.5px solid transparent; }
.screen .journey .journey-thumb { width: 92px; height: 68px; border-radius: 0;
  object-fit: cover; display: block; background: var(--gray); }
.screen .journey .journey-copy { display: grid; gap: 1px; min-width: 0; }
.screen .journey em { font-style: normal; font-family: var(--sans);
  font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--quiet); font-variant-numeric: tabular-nums; }
.screen .journey b { font-family: var(--sans); font-size: 12.5px; font-weight: 650;
  line-height: 1.25; color: var(--ink); letter-spacing: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.screen .journey small { font-size: 11px; color: var(--muted); line-height: 1.3; }
.screen .journey .journey-people { font-family: var(--sans); font-size: 11px;
  color: var(--quiet); line-height: 1.3; }

/* Attended, tonight, and not yet scheduled — one signal each. */
.screen .journey .is-done .journey-thumb { opacity: .82; }
.screen .journey .is-now { border-left-color: var(--brand); background: none; }
.screen .journey .is-now em { color: var(--brand); font-weight: 700; }
.screen .journey .is-unset .journey-thumb { visibility: hidden; }
.screen .journey .is-unset b,
.screen .journey .is-unset em { color: var(--quiet); }
.screen .journey .journey-entry:last-child { border-bottom: 0; }

/* Held rows: bigger picture, quieter type — same move, same numbers. */
.screen .held-list li > button { grid-template-columns: 92px 1fr 16px; }
.screen .held-thumb { width: 92px; height: 68px; border-radius: 0; }
.screen .held-list b { font-size: 12.5px; }
.screen .held-list em { font-size: 11px; }
.screen .held-list small { font-size: 11px; }
/* A hidden thumbnail must not leave a 92px hole where a photo would be. */
.screen .journey .journey-entry:has(.journey-thumb[hidden]),
/* A concert with no picture is still a concert row. Dropping the image column
   made it a different object beside the ones that have one — measured at
   280px+16 against 96px+200 everywhere else. The slot stays and holds a plate,
   so the copy starts on the same line down the whole list. */
.screen .held-list li > button:has(.held-thumb[hidden]) { grid-template-columns: 96px minmax(0, 1fr); }
.screen .held-list .held-thumb[hidden] {
  display: block; visibility: visible;
  width: 96px; height: 96px; min-width: 96px;
  background: var(--c-gray-100);
}
.screen .journey .journey-entry:has(.journey-thumb[hidden]) { grid-template-columns: 1fr; }

/* The old rail filled the current stop with burgundy and set its type white.
   The fill is gone (it is a rail now), so `.journey li.is-now b` was painting
   the title off-white on white — invisible, and it survived because it carries
   two elements to this file's one. Beaten explicitly, not by ordering. */
.screen .journey .journey-entry.is-now b { color: var(--ink); font-weight: 650; }
.screen .journey .journey-entry.is-now em { color: var(--brand); }

/* ---- Matched to the marketing site ----------------------------------------
   The marketing screen in this same build is the brand's own statement, and on
   two points it disagreed with what the app had just become:

   1. Its call to action is a SQUARE block — REQUEST DEMO, EXPLORE FEATURES —
      not a pill. UNIQLO keeps a pill for ADD TO CART, but a product does not
      get to hold two action shapes at once, and between the reference and the
      client's own front door the front door wins. The app's actions go square.
   2. Its light ground is `--surface`, the warm paper the phone body already
      sits on. The bar and the announcements sheet were pure `--off-white`,
      which read a half-step brighter than everything around them.

   Its feature list is already hairline-separated with no fills, which is what
   the app screens now do — that part matched on its own. */
.primary-action, .secondary-action,
.event-actions button, .btn,
.benefit-list button > span:last-child,
.mark-pill { border-radius: 0; }
.bottom-nav,
.stage-over[data-overlay-panel="announcements"],
.modal-card { background: var(--surface); }
.field input { background: var(--surface); }
.field input:focus-visible, .field input.is-hover { background: var(--surface); }
/* Nav chrome stays sentence case, the way the marketing bar sets "Log in".
   Uppercase is for the action, not for every control. */
.mkt-login { text-transform: none; letter-spacing: normal; }
/* Same `.screen` weight trap as before — the fourth time this file has caught
   me with it. The row action is a square hairline block like every other action
   in the build. */
.screen .benefit-list button > span:last-child {
  border-radius: 0;
  background: none;
  border: 1px solid var(--hairline);
}

/* ═══ CLEAN-LAYOUT CONTENT ═══════════════════════════════════════════════════
   Screens transcribed from Maestro-Clean-Layout, 8 September 2026. The layout
   is drawn dark, rounded and blue; that direction was not adopted — only its
   content. Everything below is dressed in the register the app already runs:
   warm ground, square, hairline, burgundy spent once.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Home · October challenge */
.challenge { border: 1px solid var(--hairline); padding: 14px 16px 16px;
  display: grid; gap: 7px; }
.challenge b { font-size: 15px; font-weight: 650; letter-spacing: -.01em; }
.challenge p { margin: 0; font-size: 13px; line-height: 1.45; color: var(--muted); }
.challenge-track { display: block; height: 3px; background: var(--gray); margin-top: 5px; }
.challenge-track i { display: block; height: 100%; width: var(--pct, 0%); background: var(--brand); }

/* Home · season sponsors */
.sponsor-list { list-style: none; margin: 0; padding: 0; display: grid; }
.sponsor-list li { display: grid; gap: 3px; padding: 13px 0;
  border-bottom: 1px solid var(--line); }
.sponsor-list b { font-size: 15px; font-weight: 650; }
.sponsor-list small { font-size: 12.5px; color: var(--muted); line-height: 1.45; }

/* Home · the live programme bar. Drawn floating in the layout; here it is the
   last block on Home instead, because a second floating object over a bar that
   already floats is what the screenshot review flagged for overlap. */
.program-bar { margin-top: 22px; border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line); padding: 13px 0;
  display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.program-live { display: flex; align-items: center; gap: 10px; min-width: 0; }
.program-live b { display: block; font-size: 14px; font-weight: 650; }
.program-live small { display: block; font-size: 12.5px; color: var(--muted); }
.live-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--brand);
  flex: none; }
.program-open { border: 0; background: none; cursor: pointer; flex: none;
  display: inline-flex; align-items: center; gap: 4px; min-height: 44px;
  font: inherit; font-size: 11px; font-weight: 650; text-transform: uppercase;
  letter-spacing: .085em; color: var(--ink); }
.program-open .icon { width: 14px; height: 14px; }

/* Program overlay */
.program-art { margin: 18px 18px 0; padding: 16px; background: var(--gray);
  display: grid; align-content: end; min-height: 152px; position: relative; }
.program-initial { position: absolute; inset: 0; display: grid; place-items: center;
  font: 400 88px/1 var(--display); color: color-mix(in oklch, var(--ink) 14%, transparent); }
.program-art figcaption { position: relative; display: grid; gap: 3px; }
.program-art em { font-style: normal; font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); }
.program-art b { font-size: 17px; font-weight: 650; letter-spacing: -.012em; }
.program-listen { margin: 12px 18px 0; width: calc(100% - 36px); border: 1px solid var(--hairline);
  background: none; cursor: pointer; padding: 12px 14px; display: flex;
  align-items: center; gap: 12px; text-align: left; font: inherit; }
.program-play { width: 30px; height: 30px; border-radius: 50%; background: var(--ink);
  color: var(--off-white); display: grid; place-items: center; flex: none; }
.program-play .icon { width: 13px; height: 13px; fill: currentColor; stroke: none; }
.program-listen-copy { display: grid; gap: 2px; min-width: 0; }
.program-listen-copy b { font-size: 14px; font-weight: 650; }
.program-listen-copy small { font-size: 12px; color: var(--muted); }
.modal-body .program-about { margin: 0; padding: 0 18px; font-size: 13.5px;
  line-height: 1.55; color: var(--muted); }
.modal-body .kv-list { padding: 0 18px; }

/* Events · filter menus */
.chip--menu { display: inline-flex; align-items: center; gap: 5px; }
.chip--menu .icon { width: 12px; height: 12px; }
.stage-over--menu { background: none; z-index: 46; }
.menu-scrim { position: absolute; inset: 0; border: 0; background: rgb(16 16 16 / .18);
  cursor: pointer; }
.drop-menu { position: absolute; top: 148px; left: 18px; min-width: 190px;
  background: var(--surface); border: 1px solid var(--hairline);
  box-shadow: 0 18px 46px rgb(16 16 16 / .18); display: grid; }
.drop-menu button { border: 0; background: none; text-align: left; cursor: pointer;
  font: inherit; font-size: 14px; min-height: 46px; padding: 0 16px;
  border-bottom: 1px solid var(--line); color: var(--ink); }
.drop-menu button:last-child { border-bottom: 0; }
.drop-menu button.is-on { font-weight: 650; }
.drop-menu button:active { background: var(--gray); }

/* Events · inline row actions */
.row-actions { display: flex; gap: 16px; margin-top: 6px; }
.row-actions a { font-size: 12.5px; font-weight: 600; color: var(--brand);
  cursor: pointer; text-decoration: none; }
.row-actions a:active { text-decoration: underline; text-underline-offset: 2px; }

/* Profile · badges */
.badge-rail { list-style: none; margin: 0; padding: 0 18px 2px 0; display: flex;
  gap: 8px; overflow-x: auto; scrollbar-width: none;
  width: calc(100% + 18px); }
.badge-rail::-webkit-scrollbar { display: none; }
.badge-rail li { flex: 0 0 46%; border: 1px solid var(--hairline); padding: 12px;
  display: grid; gap: 5px; align-content: start; }
.badge-ico { width: 26px; height: 26px; display: grid; place-items: center;
  border: 1px solid var(--hairline); color: var(--ink); }
.badge-ico .icon { width: 14px; height: 14px; }
.badge-rail b { font-size: 13.5px; font-weight: 650; }
.badge-rail small { font-size: 11.5px; color: var(--muted); line-height: 1.4; }

/* Profile · attendance history */
.history-list { list-style: none; margin: 0; padding: 0; }
.history-list li { display: grid; gap: 2px; padding: 13px 0;
  border-bottom: 1px solid var(--line); }
.history-list b { font-size: 15px; font-weight: 650; letter-spacing: -.01em; }
.history-list em { font-style: normal; font-size: 12.5px; font-weight: 600; }
.history-list small { font-size: 12.5px; color: var(--muted); }

/* Profile · friends */
.friend-list { list-style: none; margin: 0; padding: 0; }
.friend-list li { display: flex; align-items: center; gap: 12px; padding: 11px 0;
  border-bottom: 1px solid var(--line); }
.friend-ini { width: 32px; height: 32px; border-radius: 50%; background: var(--gray);
  display: grid; place-items: center; font-size: 11px; font-weight: 700;
  letter-spacing: .04em; color: var(--muted); flex: none; }
.friend-copy { display: grid; gap: 1px; min-width: 0; }
.friend-copy b { font-size: 14.5px; font-weight: 650; }
.friend-copy small { font-size: 12.5px; color: var(--muted); }
.connect-btn { margin-top: 12px; min-height: 40px; padding: 0 14px;
  border: 1px solid var(--hairline); background: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 8px; font: inherit;
  font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: .085em; }
.connect-btn .icon { width: 14px; height: 14px; }

/* Membership · a used benefit */
.benefit-list .is-used { color: var(--brand); font-weight: 600; }
.screen .benefit-list button > span:last-child.is-done { display: inline-flex;
  align-items: center; gap: 5px; background: var(--ink); border-color: var(--ink);
  color: var(--off-white); }
.screen .benefit-list button > span:last-child.is-done .icon { width: 12px; height: 12px; }

/* Settings */
.settings-note { margin: 0; padding: 10px 0 14px; font-size: 12.5px;
  line-height: 1.5; color: var(--muted); border-bottom: 1px solid var(--line); }
.settings-note .linkish { display: block; margin-top: 6px; font-weight: 650;
  color: var(--brand); }
.sign-out { margin-top: 20px; width: 100%; min-height: 46px; border: 1px solid var(--hairline);
  background: none; cursor: pointer; font: inherit; font-size: 11px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .085em; color: var(--brand); }
.pass-connect { display: block; margin-top: 8px; font-size: 12.5px; font-weight: 600;
  color: var(--brand); }
/* The action pair sits on one line under the row; wrapping split "View / tickets". */
.row-actions a { white-space: nowrap; }
/* The category menu is a dropdown, not a full-screen overlay: no scrim blur, and
   the page behind stays legible because the menu is what has moved forward. */
.stage-over--menu { backdrop-filter: none; -webkit-backdrop-filter: none; background: none; }
.stage-over--menu .menu-scrim { backdrop-filter: none; -webkit-backdrop-filter: none; }

/* ---- Tab bar, more air ----------------------------------------------------
   The bar was 67px with 9px of padding, which put the labels close enough to
   the device edge that the row read as cramped against the home indicator.
   14px top, 12px under the label plus the safe area, and a little more between
   glyph and label. */
.bottom-nav { padding: 14px 4px calc(12px + env(safe-area-inset-bottom, 0px)); }
.bottom-nav button { gap: 7px; min-height: 52px; }
/* Content has to clear the taller bar: 79px of bar plus a little air. */
.screen { padding-bottom: 96px; }
/* Bleed discipline for the peek rails. They are meant to run off the right edge
   so the next card shows, but the last item was landing exactly on the device
   edge (measured +1px past it), which reads as a clipped mark rather than as
   more content. The trailing gutter gives the cut somewhere to happen. */
.badge-rail { padding-right: 18px; }
.badge-rail li:last-child { margin-right: 18px; }

/* ---- The header sits in the frame -----------------------------------------
   .hero-chrome was sticky with a negative bottom margin
   (`calc(-1 * (var(--safe-top) + 52px))`) whose only job was to drag the
   photograph up underneath it. That is what made the mark and the bell float
   over the image and over the top of the device.

   It is a header now: it occupies its own band on the app ground, below the
   status area, and the hero begins where the header ends. Nothing overlaps, so
   nothing has to be pulled anywhere.

   The pull-up was also the scaling fault. The margin was computed from
   --safe-top (itself a max() against the viewport) while .hero was sized from
   a second expression, so the two drifted apart at different heights and the
   photograph either left a band above it or swallowed the chrome. The header
   is now measured by its content and the hero by its own box; neither depends
   on the other's arithmetic. */
.hero-chrome {
  position: relative;
  margin: 0;
  padding: calc(var(--safe-top) - 18px) 18px 12px;
  background: var(--surface);
  z-index: 20;
}
.screen:has(> .hero-chrome) { padding-top: 0; }
/* The hero is a block in the flow, sized against the device rather than the
   viewport, so it holds its proportion at any frame height. */
.hero {
  height: 38%;
  min-height: 0;
  max-height: 340px;
  margin: 0 0 4px;
}
/* The hero is in the flow now, so it picks up the screen's 18px gutter and had
   become a card on a ground. It is a photograph, and photographs run to the
   edge: give back the gutter on both sides rather than reintroducing the
   pull-up that caused the overlap. */
.hero { margin-left: -18px; margin-right: -18px; width: calc(100% + 36px); }
/* The membership rail kept its filled capsule through the whole register pass —
   it is the last tile on the app. Hairline box, square, same as every other row. */
.member-card { background: none; border: 1px solid var(--hairline); border-radius: 0; }

/* ═══ TYPE SCALE ════════════════════════════════════════════════════════════
   From the Maestro brand/web Figma text styles. Recorded whole — both columns —
   so the scale is a token set rather than a screenshot someone has to remember.
   Values are size/line-height, line-height as a percentage, exactly as the
   styles are named.

   This closes the gap TOKENS.md called blocking: "No mobile scale. D1's
   variables were already namespaced Desktop/, implying a mobile set that was
   never built. Needed before responsive work." It exists now.

   The prototype is a phone, so it is dressed from the Mobile column. The
   Desktop column is carried unused, for the marketing screen and the site.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* shared across both columns */
  --t-eyebrow:        14px;   --t-eyebrow-lh:        1.40;
  --t-caption:        16px;   --t-caption-lh:        1.40;  /* see note below */
  --t-button:         16px;   --t-button-lh:         1.00;

  /* mobile — what this app uses */
  --t-hero:           48px;   --t-hero-lh:           1.10;
  --t-heading-lg:     42px;   --t-heading-lg-lh:     1.10;
  --t-heading-md:     34px;   --t-heading-md-lh:     1.10;
  --t-heading-sm:     28px;   --t-heading-sm-lh:     1.10;
  --t-subheading-lg:  22px;   --t-subheading-lg-lh:  1.10;
  --t-subheading-md:  20px;   --t-subheading-md-lh:  1.10;
  --t-body-lg:        18px;   --t-body-lg-lh:        1.20;
  --t-body-md:        16px;   --t-body-md-lh:        1.20;
  --t-quote:          32px;   --t-quote-lh:          1.25;  /* named Auto */

  /* desktop — carried, not applied here */
  --t-d-hero:          72px;  --t-d-heading-lg:      64px;
  --t-d-heading-md:    56px;  --t-d-heading-sm:      48px;
  --t-d-subheading-lg: 36px;  --t-d-subheading-md:   24px;
  --t-d-body-lg:       20px;  --t-d-body-md:         18px;
  --t-d-quote:         48px;
}

/* Screen titles — Heading SM. */
.screen .app-header h1 { font-size: var(--t-heading-sm); line-height: var(--t-heading-sm-lh); }
/* The one editorial moment on Home — Heading MD. */
.hero-title { font-size: var(--t-heading-md); line-height: var(--t-heading-md-lh); }
.home-greet { font-size: var(--t-heading-sm); line-height: var(--t-heading-sm-lh); }
/* Group names that read as headings, not labels — Subheading MD. */
.rail-head h2, .event-group-name, .modal-head h2 {
  font-size: var(--t-subheading-md); line-height: var(--t-subheading-md-lh); }
/* The uppercase band labels are the Eyebrow style. */
.screen .section-heading h2, .screen .section-heading span {
  font-size: var(--t-eyebrow); line-height: var(--t-eyebrow-lh); }
.screen .app-header .date { font-size: var(--t-eyebrow); line-height: var(--t-eyebrow-lh); }
/* Row titles — Body LG. Row supporting text — Body MD. */
.screen .held-list b, .modal-card .notice-copy b, .history-list b,
.friend-copy b, .challenge b, .sponsor-list b, .program-listen-copy b {
  font-size: var(--t-body-lg); line-height: var(--t-body-lg-lh); }
.screen .held-list em, .screen .held-list small,
.modal-card .notice-copy span, .history-list em, .history-list small,
.friend-copy small, .challenge p, .sponsor-list small,
.program-listen-copy small, .row-actions a {
  font-size: var(--t-body-md); line-height: var(--t-body-md-lh); }

/* ---- The category menu is a dropdown, not a modal -------------------------
   It was rendering as a full-screen overlay: a dimmed scrim over the whole
   phone and a panel floating across the screen title. A dropdown belongs under
   the control that opened it, leaves the page undimmed, and is only as big as
   its options. Position is set on open in app.js, because the filter row
   scrolls and the chip moves. */
.stage-over--menu { background: none; backdrop-filter: none; -webkit-backdrop-filter: none; }
.menu-scrim { background: none; }
.drop-menu {
  top: 0; left: 18px;                     /* replaced on open */
  min-width: 176px; max-width: 220px;
  box-shadow: 0 10px 28px rgb(16 16 16 / .16);
}
.drop-menu button {
  min-height: 40px; padding: 0 14px;
  font-size: 15px; line-height: 1.2;
}

/* ---- Row actions stay under the row -------------------------------------
   At Body MD the pair measured 49px wider than its grid track. The scale has
   no step between Eyebrow and Body MD, so the actions take Eyebrow: they are
   labels on a row, not body copy. */
.row-actions a { font-size: var(--t-eyebrow); line-height: 1.2; }
.screen .held-list .held-copy { overflow: hidden; }
.screen .held-list b { white-space: normal; overflow-wrap: anywhere; }

/* ---- The creative fills the top bar ---------------------------------------
   The header keeps its own band — it does not float over the page, which was
   the earlier fault — but its ground is the photograph rather than white. The
   chrome now lives inside .hero, so the image fills header and hero as one
   block and there is nothing to pull upward.

   A short scrim under the top edge keeps the mark and the bell legible when the
   photograph is bright there; it stops well before the hero copy so it does not
   double up with the veil already over the lower half. */
.hero { position: relative; overflow: hidden; }
.hero-chrome {
  position: absolute; top: 0; left: 0; right: 0;
  background: none;
  padding: calc(var(--safe-top) - 18px) 18px 12px;
  z-index: 3;
}
.hero::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 132px;
  background: linear-gradient(to bottom, rgb(10 10 10 / .34), transparent);
  z-index: 2; pointer-events: none;
}
.hero-img { position: absolute; inset: 0; z-index: 1; }
.hero-veil, .hero-body { z-index: 2; }
/* The hero is the first thing in the screen now, so the screen keeps no top
   padding of its own. */
.screen:has(> .hero) { padding-top: 0; }

/* ---- The membership card reads as a card ----------------------------------
   It had the structure of one and none of the signals: a --hairline edge that
   is nearly invisible on the warm ground, no separation before the stat row,
   and padding tight enough that the content sat against the boundary rather
   than inside it. So it read as a loose column of text that happened to have a
   line near it.

   A card in this register is not a fill — it is a defined edge with real inner
   space and one internal rule where the summary turns into figures. That rule
   is the same hairline the lists use, so the card is made of the same parts as
   everything else. */
.member-card {
  border: 1px solid var(--line);
  padding: 18px 18px 20px;
  gap: 0;
}
.member-card:active { background: var(--gray); }
.member-board {
  border-top: 1px solid var(--line);
  padding-top: 14px;
  margin-top: 18px;
  gap: 10px 14px;
}
/* The issuer block is the card's masthead, so it gets its own rule under it
   rather than floating above the kicker. */
.member-head {
  padding-bottom: 13px;
  border-bottom: 1px solid var(--line);
}
.member-kicker { margin-top: 13px; }

/* ---- B2B2C: the app belongs to the orchestra -------------------------------
   Maestro is the vendor; the patron's app is the CSO's. The Maestro lockup
   stays where Maestro is the party being addressed — the prototype bar, the
   marketing screen, and the identity documentation — and comes off the three
   patron-facing surfaces: splash, the home header, and the pass.
   The mark is set the way the membership cards already set the issuer, so the
   two agree rather than introducing a second CSO treatment. */
.cso-lockup { display: inline-flex; flex-direction: column; gap: 2px; color: inherit; }
.cso-lockup b { font-family: var(--sans); font-size: 15px; font-weight: 750;
  letter-spacing: .02em; line-height: 1; }
.cso-lockup em { font-style: normal; font-family: var(--sans); font-size: 8px;
  font-weight: 600; letter-spacing: .14em; text-transform: uppercase;
  opacity: .72; line-height: 1; }
.cso-lockup.is-splash b { font-size: 34px; letter-spacing: .01em; }
.cso-lockup.is-splash em { font-size: 10px; letter-spacing: .18em; margin-top: 6px; }
.mark-pill .cso-lockup b { font-size: 14px; }

/* ---- The hairline was drawing the wrong way --------------------------------
   --hairline is `oklch(1 0 0 / .5)` — white at half opacity, and its own
   comment says "light hairline on tinted grounds". It was correct while these
   components sat on a --raised fill. The register pass took the fills away, so
   every one of these borders became a *lighter* line on a warm ground, where a
   darker one is what reads as an edge. That is why the membership card looked
   like loose text near a line rather than a card.

   Everything that now sits on the app ground takes --line. --hairline keeps its
   job on the tinted and dark surfaces. */
.chip, .filter-row button,
.field input,
.connect-btn, .sign-out,
.badge-rail li, .program-listen,
.plan-strip, .renew-block,
.choice-icon, .drop-menu,
.screen .benefit-list button > span:last-child,
.segments button { border-color: var(--line); }
.field input:focus-visible, .field input.is-hover { border-color: var(--quiet); }
.chip.is-on, .filter-row button.is-on,
.segments button.is-active { border-color: var(--ink); }

/* ---- The logo is the orchestra's ------------------------------------------
   The app is the CSO's, 100%, so the lockup is a CSO wordmark and not Maestro's
   bookmark glyph with CSO lettered beside it. Putting the vendor's mark on the
   constituent-facing app is the one thing B2B2C rules out.

   Set in the display face so it reads as a mark rather than as a bold label —
   the same face the hero and the member names use, so it belongs to the app it
   sits on.

   ⚠ This is a typographic stand-in. The real CSO logo (a lowercase serif `cso`
   in the client's own artwork) is not in this repo. Drop the asset in and
   replace .cso-lockup b — do not redraw it. */
.cso-lockup b {
  font-family: var(--display);
  font-weight: 400;
  font-size: 21px;
  letter-spacing: .06em;
  line-height: 1;
}
.mark-pill .cso-lockup b { font-size: 19px; }
.cso-lockup.is-splash b { font-size: 46px; letter-spacing: .05em; }

/* ---- The issuer came off the membership cards -----------------------------
   The app is the CSO's now — the header says so on every screen — so a card
   restating CSO / CINCINNATI SYMPHONY ORCHESTRA was telling the reader
   something they had already been told twice. The masthead rule went with it:
   it existed to separate the issuer from the kicker, and there is no issuer.

   .member-head stays only where it carries a state badge, and collapses when
   it does not, so the card starts on its kicker rather than on a blank band. */
.member-head { padding-bottom: 0; border-bottom: 0; }
.member-head:empty { display: none; }
.member-kicker { margin-top: 0; }
.member-head:not(:empty) + .member-kicker { margin-top: 12px; }
/* The state badge was the last pill left on the app — square, like every other
   chip and selector in the register. */
.member-state { border-radius: 0; background: none; border: 1px solid var(--line); }

/* ---- The real CSO logo ----------------------------------------------------
   Client artwork, 185×45, replacing the typographic stand-in. The lockup is the
   lowercase `cso` mark with CINCINNATI SYMPHONY ORCHESTRA set in three lines
   beside it — the full name is *inside the artwork*, which is why the separate
   name line came off the splash rather than being repeated.

   ⚠ **The file is white-on-transparent.** Every pixel in it is #FFFFFF, so it
   can only ever sit on a dark ground. All three placements are: the splash
   photograph, the home header pill (--off-black at 82%) and the pass. Putting
   this file on the app's warm ground renders nothing at all. A dark version is
   needed before the mark can go anywhere else.

   ⚠ It is a 1x PNG. At splash size it renders near its native width, so it will
   soften on a retina screen. Ask the client for the SVG, or a 2x export. */
.cso-logo { display: block; width: auto; height: 16px; }
.mark-pill .cso-logo { height: 15px; }
.cso-lockup.is-splash .cso-logo { height: 42px; }
.cso-lockup { display: inline-flex; align-items: center; }

/* ═══ FIGMA BRAND VARIABLES ═════════════════════════════════════════════════
   Read from the Maestro brand/web Figma (VHgUwNO6lv8DynIHEgc30m) as the client
   publishes them. Recorded whole so the system is a token set, not a set of
   screenshots.

   ⚠ TWO CONTRADICTIONS WITH WHAT THIS BUILD WAS TOLD EARLIER. Both are real and
   neither is silently resolved here — see CONTEXT.md.

   1. RADIUS. `tokens.css` in the client folder carries a directed decision
      dated 2026-08-24: "The system is square", radius-sm/md 0. This file
      publishes Container 16, Inside Container 8, Input 8, Button 8, Round 999,
      Label 99. Those are different systems. The brand file is the live one and
      is adopted here, which reverses the square pass.
   2. RED. The same `tokens.css` samples brand burgundy as #390C0C, and the
      house memory says Maestro is always burgundy. This file publishes
      Maestro Red #690218 — a deeper crimson, not that brown-burgundy. Adopted
      as published.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Color / Primary */
  --c-white:        #FFFFFF;
  --c-black:        #060607;
  --c-maestro-red:  #690218;
  --c-maestro-blue: #B9D7F6;

  /* Color / Neutrals — also Color / UI / Gray */
  --c-gray-100: #F6F5F3;  --c-gray-200: #CBCAC8;  --c-gray-300: #969593;
  --c-gray-400: #50504E;  --c-gray-500: #2D2D2C;

  /* Color / UI */
  --c-red-100: #E5A1A3;   --c-red-200: #D16265;   --c-red-300: #A01F3B;
  --c-red-400: #690218;   --c-red-500: #3A000D;
  --c-blue-100: #DBE8F6;  --c-blue-200: #B9D7F6;  --c-blue-300: #8EB3D9;
  --c-blue-400: #658DB5;  --c-blue-500: #365B82;
  --c-yellow-100: #FFE4AE; --c-yellow-200: #F3CF85; --c-yellow-300: #D7B063;
  --c-yellow-400: #B38A39; --c-yellow-500: #87641F;
  --c-green-100: #CADEB4; --c-green-200: #B4C699; --c-green-300: #94A77A;
  --c-green-400: #7A8C61; --c-green-500: #606E46;

  /* Radius */
  --radius-round: 999px;  --radius-module: 0;      --radius-container: 16px;
  --radius-inside: 8px;   --radius-input: 8px;     --radius-button: 8px;
  /* One scale, no strays: stamps and chips at 4, anything with content inside
     it at 8, cards and sheets at 16, pills fully round. --radius-label used to
     be 99px, which is the same pill drawn with a second number. */
  --radius-stamp: 4px;
  --radius-label: 999px;

  /* Spacing — fixed 8 through 128, step 8 */
  --sp-8: 8px;    --sp-16: 16px;  --sp-24: 24px;  --sp-32: 32px;
  --sp-40: 40px;  --sp-48: 48px;  --sp-56: 56px;  --sp-64: 64px;
  --sp-72: 72px;  --sp-80: 80px;  --sp-88: 88px;  --sp-96: 96px;
  --sp-104: 104px; --sp-112: 112px; --sp-120: 120px; --sp-128: 128px;
}

/* ---- The app's own tokens now resolve to the brand system ----------------- */
:root {
  --brand:   var(--c-maestro-red);
  --off-black: var(--c-black);
  --ink:     var(--c-black);
  --muted:   var(--c-gray-400);
  --quiet:   var(--c-gray-300);
  --line:    var(--c-gray-200);
  --gray:    var(--c-gray-100);
  --raised:  var(--c-gray-100);
  --surface: var(--c-gray-100);
  --r-pill:  var(--radius-round);
  --r-field: var(--radius-input);
  --r-card:  var(--radius-container);
  --r-chip:  var(--radius-label);
}

/* ---- Radius by role, per the published tokens -----------------------------
   The square pass set literal zeros on these; the brand file gives each one a
   value, so they are restated by role rather than left at 0. */
.primary-action, .secondary-action, .event-actions button, .btn,
.mkt-cta, .mkt-login, .sign-out, .connect-btn,
.screen .benefit-list button > span:last-child { border-radius: var(--radius-button); }
.field input { border-radius: var(--radius-input); }
.member-card, .modal-card, .program-art, .pass-ticket,
.plan-strip, .renew-block, .badge-rail li, .program-listen,
.action-row, .compact-grid button, .support-note,
.drop-menu { border-radius: var(--radius-container); }
.pass-code-zone, .choice-icon, .held-thumb, .journey-thumb,
.screen .held-thumb { border-radius: var(--radius-inside); }
.chip, .filter-row button, .segments button, .member-state,
.tier { border-radius: var(--radius-label); }
.segments { border-radius: var(--radius-label); overflow: hidden; }
.segments button { margin-left: 0; }

/* ---- Splash, from Figma node 11024:2294 -----------------------------------
   Frame 420×846 on Color/Primary/Black, a centred column at Spacing/fixed-24
   with Spacing/fixed-48 between the three blocks: the customer mark, the stage
   photograph in a Radius/Container box, and the Powered by signature.

   The old build had a full-bleed blurred photograph with a shade over it and a
   line of copy. Both are gone — the design carries no headline, and the
   photograph is a contained object here, not a background. */
.splash-screen {
  background: var(--c-black);
  display: flex; align-items: center; justify-content: center;
  padding: 0 var(--sp-24);
  overflow: hidden;
}
.splash-stack {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--sp-48);
  width: 100%; max-width: 372px;
}
.splash-mark { display: block; width: 159px; height: auto; }
.splash-stage {
  margin: 0; width: 100%;
  border-radius: var(--radius-container);
  overflow: hidden;
}
.splash-stage img { display: block; width: 100%; height: auto; }
/* Caption: 16px over a 14px line, per the style attached to this node. */
.splash-powered {
  margin: 0;
  display: flex; flex-direction: column; align-items: center;
  gap: 12px;
  color: var(--c-white);
  font-family: var(--sans);
  font-size: var(--t-caption);
  line-height: 14px;
}
.splash-maestro { display: block; width: 135px; height: 16px; }
/* The previous splash centred its stack with `position:absolute` + a
   translateY(-42%) nudge. The Figma frame centres a normal flex column, so the
   stack goes back into flow — otherwise its children collapse to 0×0 and the
   Powered by signature never reaches the bottom of the frame. */
.splash-stack {
  position: static;
  left: auto; right: auto; top: auto;
  transform: none;
  text-align: center;
}
.splash-stack p { margin: 0; font-size: var(--t-caption); color: var(--c-white); }

/* ---- Home header: a scrim, not a plate -----------------------------------
   The mark sat in a filled black rectangle over the photograph — a badge with
   its own edges competing with the image behind it. The legibility job belongs
   to a gradient over the top of the creative, which is invisible as an object
   and lets the mark sit directly on the picture. The bell keeps its disc,
   because a round control reads as a control. */
.mark-pill {
  background: none;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  padding: 0;
  height: auto;
  border-radius: 0;
}
.hero::before {
  height: 180px;
  background: linear-gradient(to bottom,
    rgb(6 6 7 / .72) 0%,
    rgb(6 6 7 / .42) 46%,
    rgb(6 6 7 / 0) 100%);
}

/* ---- The challenge card kept the inverted hairline ------------------------
   It was written in the clean-layout pass with `var(--hairline)` and was not on
   the list when those were corrected to --line, so it was still drawing a white
   edge on a light ground. Same fix, and the progress rail gets its own room
   rather than sitting on the card's bottom padding. */
.challenge { border-color: var(--line); border-radius: var(--radius-container);
  padding: 16px 18px 18px; }
.challenge-track { height: 4px; margin-top: 10px; border-radius: var(--radius-round);
  background: var(--c-gray-200); overflow: hidden; }
.challenge-track i { border-radius: inherit; background: var(--brand); }

/* ---- Bottom bar, to the reference ----------------------------------------
   White ground rather than the app's warm paper, one hairline holding it off
   the content, no shadow — the bar is the bottom edge of the screen. Outline
   glyph for the four you are not on, solid for the one you are, and the label
   sits at caption size under it. */
.bottom-nav {
  background: var(--c-white);
  border-top: 1px solid var(--line);
  border-radius: 0;
  padding: 12px 4px calc(12px + env(safe-area-inset-bottom, 0px));
}
.bottom-nav button { gap: 6px; font-size: 10.5px; font-weight: 500; }
.bottom-nav button .icon { width: 23px; height: 23px; }
.bottom-nav button.is-active { color: var(--brand); }
.bottom-nav button:not(.is-active) { color: var(--c-gray-400); }

/* ---- The bar clears the home handle ---------------------------------------
   The padding was `calc(12px + env(safe-area-inset-bottom, 0px))`, and that
   env() resolves to **0 in the mock** — a browser in a drawn phone frame has no
   safe area to report. So the labels ran right down to the frame edge, straight
   through where the handle sits.

   `--safe-bottom` is `max(env(safe-area-inset-bottom), 30px)` and exists for
   this: the real inset on a device, a 30px floor everywhere else. The identity
   docs already say it — "Home indicator. The tab bar sits above it" — the bar
   just was not using the token that makes it true. */
.bottom-nav {
  padding-bottom: calc(var(--safe-bottom) - 4px);
  padding-top: 12px;
}
/* Content clears the taller bar. */
.screen { padding-bottom: calc(84px + var(--safe-bottom)); }
/* Smaller caption under the glyph. The icon carries the recognition, so the
   label can sit below caption size — and it buys back room between the two long
   destinations, which were the tightest pair in the bar. */
.bottom-nav button { font-size: 9.5px; letter-spacing: 0; }

/* ---- The live programme bar floats ----------------------------------------
   It was an inline block with a rule above and below, which is where the extra
   dividers came from: the sponsor list closed with a hairline, the bar opened
   with one, and closed with a third. Three rules in 60px.

   It is a floating component now — a dark capsule that sticks above the tab bar
   while the page scrolls under it, the way the reference draws it. Sticky
   rather than fixed so it stays inside the scrollport and cannot escape the
   device frame. */
.program-bar {
  position: sticky;
  bottom: calc(var(--safe-bottom) + 74px);
  z-index: 12;
  margin: 22px 0 0;
  border: 0;
  border-radius: var(--radius-round);
  background: var(--c-gray-500);
  color: var(--c-white);
  padding: 11px 14px 11px 16px;
  box-shadow: 0 10px 28px rgb(6 6 7 / .28);
}
.program-bar b { color: var(--c-white); font-size: 13.5px; }
.program-bar small { color: var(--c-gray-200); font-size: 12.5px; }
.live-dot { background: var(--c-red-200); }
.program-open { color: var(--c-white); font-size: 11px; min-height: 0; }
.program-open .icon { width: 13px; height: 13px; }
/* The sponsor list no longer closes with a rule — the capsule below it is its
   own object and does not need one to be separated from the text. */
.sponsor-list li:last-child { border-bottom: 0; }
/* The module is a control now — it opens the benefits it is talking about — so
   it takes a button's reset and keeps the card's shape. */
.challenge { width: 100%; text-align: left; cursor: pointer; font: inherit;
  background: none; display: grid; gap: 7px; }
/* Out of the scroll flow. As a sticky child of .screen it was being lifted from
   its flow position up over the sponsor copy whenever Home's content ran short
   — which is what sticky is supposed to do, and the wrong behaviour for a
   floating control. It is a sibling of the tab bar now, absolutely positioned
   in the device, so it sits in the same place regardless of scroll or content
   length, and only on the screen that owns it. */
.program-bar {
  position: absolute;
  left: 14px; right: 14px;
  bottom: calc(var(--safe-bottom) + 74px);
  margin: 0;
  z-index: 12;
  display: none;
}
.app-view:has(.screen[data-screen="home"]:not([hidden])) .program-bar { display: flex; }
.app-view:has(.splash-screen:not([hidden])) .program-bar { display: none; }
/* Scoped to .phone: there is no .app-view element in this build — the chain is
   phone > stage — so the previous selector matched nothing and the bar stayed
   hidden. */
.phone:has(.screen[data-screen="home"]:not([hidden])) .program-bar { display: flex; }
.phone:has(.splash-screen:not([hidden])) .program-bar { display: none; }
/* Home alone carries the floating capsule, so Home alone reserves room for it —
   otherwise the last section scrolls to a stop underneath it. */
.screen[data-screen="home"] { padding-bottom: calc(var(--safe-bottom) + 152px); }

/* ---- Announcements is a dropdown ------------------------------------------
   It was a full-screen sheet for three notices — the whole device given over to
   a list that ends a third of the way down, which is what left two thirds of it
   empty. It belongs under the bell that opens it: a panel the width of a
   dropdown, as tall as its contents, with the page still visible behind. */
.stage-over[data-overlay-panel="announcements"] {
  background: none; backdrop-filter: none; -webkit-backdrop-filter: none;
}
.modal-card.drop-panel {
  position: absolute;
  inset: auto;
  width: min(320px, calc(100% - 28px));
  max-height: 62%;
  background: var(--c-white);
  border: 1px solid var(--line);
  border-radius: var(--radius-container);
  box-shadow: 0 18px 44px rgb(6 6 7 / .22);
  overflow: hidden;
}
.drop-panel .modal-head { padding: 12px 6px 12px 16px; }
.drop-panel .modal-head h2 { font-size: var(--t-subheading-md); }
.drop-panel .modal-body { padding: 0 0 8px; overflow: auto; }
.drop-panel .modal-body .section-heading { padding: 14px 16px 6px; }
.drop-panel .notice-item { padding: 12px 16px; }
.drop-panel .notice-item:last-child { border-bottom: 0; }
.drop-panel .notice-copy b { font-size: var(--t-body-md); }
.drop-panel .notice-copy span { font-size: 13px; }

/* ---- Filter sheet, after the UNIQLO components ----------------------------
   Their filter is a sheet with four parts: the applied filters as dismissible
   chips with a Clear all beside them, the facets as accordion rows that expand
   in place, and a footer that states the result count next to the action. The
   count is the part most filter sheets leave out and the reason theirs is good
   — you can see what the filter costs you before you commit to it.

   The "More" chip opened nothing before this. */
.filter-sheet { max-height: 78%; display: flex; flex-direction: column; padding-bottom: 0; }
.filter-title { margin: 2px 0 14px; text-align: center;
  font-size: var(--t-subheading-md); font-weight: 650; }
.applied-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  padding-bottom: 14px; }
.applied { display: inline-flex; align-items: center; gap: 7px;
  min-height: 34px; padding: 0 12px;
  border: 1px solid var(--line); border-radius: var(--radius-label);
  background: none; cursor: pointer; font: inherit; font-size: 13px; }
.applied .icon { width: 12px; height: 12px; color: var(--muted); }
.clear-all { margin-left: auto; font-size: 13px; font-weight: 600; color: var(--brand); }
.accordion { flex: 1; overflow: auto; border-top: 1px solid var(--line); }
.accordion details { border-bottom: 1px solid var(--line); }
.accordion summary { display: flex; align-items: center; justify-content: space-between;
  min-height: 52px; cursor: pointer; list-style: none;
  font-size: var(--t-body-md); font-weight: 500; }
.accordion summary::-webkit-details-marker { display: none; }
/* The sign is the control's whole state: + closed, − open. */
.acc-sign { position: relative; width: 14px; height: 14px; flex: none; }
.acc-sign::before, .acc-sign::after { content: ""; position: absolute; background: var(--ink);
  left: 50%; top: 50%; transform: translate(-50%, -50%); }
.acc-sign::before { width: 14px; height: 1.5px; }
.acc-sign::after { width: 1.5px; height: 14px; transition: opacity 160ms ease; }
.accordion details[open] .acc-sign::after { opacity: 0; }
.acc-body { display: grid; gap: 2px; padding: 2px 0 14px; }
.acc-body label { display: flex; align-items: center; gap: 12px; min-height: 40px;
  font-size: var(--t-body-md); cursor: pointer; }
.acc-body input { width: 19px; height: 19px; accent-color: var(--brand); flex: none; }
.filter-foot { display: flex; align-items: center; justify-content: space-between;
  gap: 16px; border-top: 1px solid var(--line);
  padding: 14px 0 calc(14px + var(--safe-bottom)); }
.filter-count { font-size: var(--t-body-md); color: var(--muted); }
.filter-apply { border-radius: var(--radius-round); min-width: 160px; }
/* The sheet panel was frosted, so colour from the page washed through the
   filter list. A sheet that holds controls needs its own ground. */
.sheet-panel { background: var(--c-white); backdrop-filter: none; -webkit-backdrop-filter: none; }

/* ---- The hero sizes to what it holds --------------------------------------
   The header is absolutely positioned inside the hero and the copy was
   absolutely anchored to its bottom, while the hero itself was asserted at 38%
   of the frame. So neither element contributed to the height, and whenever
   38% came out shorter than header + copy the copy rode up underneath the mark:
   measured −27px at an 874px frame, −47px at 822, −92px at 702. It got worse
   the smaller the device, which is the signature of a height that is asserted
   rather than derived.

   The copy goes back into flow and the hero grows to fit it, with the top
   padding reserving the header's band. 38% becomes a minimum, not a ceiling. */
.hero {
  height: auto;
  min-height: 38%;
  max-height: none;
  padding: calc(var(--safe-top) + 48px) 0 0;
  box-sizing: border-box;
}
.hero-body {
  position: static;
  padding: 0 18px 18px;
}
/* In flow, but still positioned: z-index only applies to positioned elements,
   so a static .hero-body dropped behind the absolutely-placed photograph. */
.hero-body { position: relative; z-index: 2; }
/* The old absolute rule still carried left/right/bottom offsets, and relative
   positioning made them apply again — shifting the copy 16px back up under the
   mark. Offsets cleared; the padding does the spacing now. */
.hero-body { inset: auto; }

/* ---- Event rows: give the copy the room it needs --------------------------
   Measured on the live row: the grid was `92px 178px 16px`. The thumbnail
   column had drifted to 92px, leaving the copy 178px while the action pair
   needs 201px — so "Add to calendar" was being clipped by the copy's
   `overflow: hidden`. And `small` carried `text-overflow: ellipsis` with no
   `overflow: hidden` of its own, which is why the venue line cut mid-word
   instead of ellipsing.

   64px thumb, 12px gutters: the copy gets ~206px and the actions fit. */
.screen .held-list li > button {
  grid-template-columns: 64px minmax(0, 1fr) 16px;
  column-gap: 12px;
}
.screen .held-thumb { width: 64px; height: 64px; }
.screen .held-list em,
.screen .held-list small {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: block;
}
/* The date was set in brand red on every row, which spends the one accent five
   times before you reach the actions that actually lead somewhere. The date is
   information; the actions are links. */
.screen .held-list em { color: var(--ink); font-weight: 600; }
.row-actions { gap: 14px; }

/* ---- Search field ---------------------------------------------------------
   The magnifier opened nothing before. It now reveals a field that filters the
   rows already on screen — it does not imply a catalogue search this prototype
   cannot perform. */
.search-field {
  display: flex; align-items: center; gap: 10px;
  margin: 0 0 12px;
  padding: 0 12px;
  min-height: 44px;
  background: var(--c-gray-100);
  border: 1px solid var(--line);
  border-radius: var(--radius-input);
}
.search-field .icon { width: 17px; height: 17px; color: var(--quiet); flex: none; }
.search-field input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none;
  font: inherit; font-size: var(--t-body-md); color: var(--ink);
  padding: 0; min-height: 42px;
}
.search-field input::-webkit-search-cancel-button { display: none; }
.search-clear { border: 0; background: none; cursor: pointer; padding: 4px;
  display: inline-flex; color: var(--quiet); }
.search-empty { margin: 18px 0; font-size: var(--t-body-md); color: var(--muted); }

/* ---- Segmented control ----------------------------------------------------
   A recessed track with the selection riding on top of it as a raised thumb,
   per the reference — not a filled black segment. The track carries the group;
   the thumb carries the state. */
.segments {
  background: var(--c-gray-100);
  border: 1px solid var(--line);
  border-radius: var(--radius-label);
  padding: 3px;
  gap: 0;
  overflow: visible;
}
.segments button {
  border: 0;
  border-radius: var(--radius-label);
  background: none;
  color: var(--muted);
  font-weight: 500;
  min-height: 34px;
}
.segments button.is-active {
  background: var(--c-white);
  color: var(--ink);
  font-weight: 650;
  box-shadow: 0 1px 3px rgb(6 6 7 / .12), 0 0 0 0.5px rgb(6 6 7 / .06);
}

/* ---- The three buttons, on the design system ------------------------------
   Radius/Button 8 for all three. Primary carries the brand fill, secondary is
   the same shape drawn rather than filled, tertiary is the label alone. One
   shape, three weights of emphasis. */
.btn--primary {
  background: var(--brand); color: var(--c-white); border: 1px solid var(--brand);
  border-radius: var(--radius-button);
}
.btn--primary:active { background: var(--c-red-500); border-color: var(--c-red-500); }
.btn--secondary {
  background: none; color: var(--ink); border: 1px solid var(--ink);
  border-radius: var(--radius-button);
}
.btn--secondary:active { background: var(--c-gray-100); }
.btn--tertiary {
  background: none; color: var(--brand); border: 1px solid transparent;
  border-radius: var(--radius-button);
}
.btn--tertiary:active { background: var(--c-gray-100); }

/* ---- Row actions are small grey buttons -----------------------------------
   They were red text links, which spent the accent twice per row before the
   eye reached anything that matters. A quiet grey control reads as an action
   without competing with the row it belongs to. */
.row-btn {
  min-height: 30px; padding: 0 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-button);
  background: var(--c-gray-100);
  color: var(--ink);
  font: inherit; font-size: 12.5px; font-weight: 600;
  cursor: pointer; white-space: nowrap;
}
.row-btn:active { background: var(--c-gray-200); }
.row-actions { gap: 8px; margin-top: 9px; }
/* A row action cannot be a <button> — it sits inside the row's own <button>,
   and the parser closes the outer one, which broke the row apart into
   full-width blocks. Spans with a button role, styled the same. */
.row-btn { display: inline-flex; align-items: center; }
/* Measured: the pair came to 221px against a 210px copy column, so the second
   button clipped. Tighter padding and a step down in size brings them inside it
   with the labels intact — shortening "Add to calendar" would have cost more
   than it saved. */
.row-btn { padding: 0 9px; font-size: 12px; min-height: 28px; }
.row-actions { gap: 7px; }

/* ---- The programme flips up from its bar ----------------------------------
   It opened as a full-screen panel from the top, which is the wrong direction
   for a control that lives at the bottom of the screen and points its chevron
   upward. It rises from the bar instead: same insets, same corner radius,
   growing up from where it was tapped and stopping short of the header so the
   page it belongs to stays in view. */
.stage-over--up { background: rgb(6 6 7 / .28); backdrop-filter: none; -webkit-backdrop-filter: none; }
.stage-over--up .menu-scrim { background: none; }
.modal-card.sheet-up {
  position: absolute;
  inset: auto 14px calc(var(--safe-bottom) + 74px) 14px;
  max-height: 72%;
  display: flex; flex-direction: column;
  background: var(--c-white);
  border: 1px solid var(--line);
  border-radius: var(--radius-container);
  box-shadow: 0 18px 46px rgb(6 6 7 / .26);
  overflow: hidden;
  transform-origin: bottom center;
  animation: sheet-rise 240ms var(--ease);
}
@keyframes sheet-rise {
  from { opacity: 0; transform: translateY(14px) scaleY(.96); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) { .modal-card.sheet-up { animation: none; } }
.sheet-up .modal-head { padding: 12px 6px 12px 16px; border-bottom: 1px solid var(--line); }
.sheet-up .modal-head h2 { font-size: var(--t-subheading-md); }
.sheet-up .modal-body { padding: 0 0 16px; overflow: auto; }
.sheet-up .program-art { margin: 14px 16px 0; }
.sheet-up .program-listen { margin: 12px 16px 0; width: calc(100% - 32px); }
.sheet-up .modal-body .kv-list,
.sheet-up .modal-body .program-about { padding-left: 16px; padding-right: 16px; }
.sheet-up .modal-body .section-heading { padding: 18px 16px 6px; }
/* The bar is what opened it, so it steps aside while the panel stands in its
   place rather than sitting behind it. */
.phone:has([data-overlay-panel="program"]:not([hidden])) .program-bar { display: none; }

/* ---- Announcements is a full modal ----------------------------------------
   Back to the full surface, deliberately: a dropdown made the notices feel
   incidental, and these carry a renewal deadline and a programme change. It
   fills the device, on its own solid ground, square to the frame, with the
   rows as hairline-separated list items — the empty space below the last one
   reads as the end of a page rather than a hole in a card, which is what made
   the original frosted version look broken. */
.modal-card.is-full {
  position: absolute;
  inset: 0;
  width: auto; max-height: none;
  display: flex; flex-direction: column;
  background: var(--c-white);
  border: 0;
  border-radius: 0;
  box-shadow: none;
  animation: none;
}
.is-full .modal-head {
  padding: calc(var(--safe-top) - 18px) 6px 14px 18px;
  border-bottom: 1px solid var(--line);
}
.is-full .modal-head h2 { font-size: var(--t-heading-sm); }
.is-full .modal-body { flex: 1; overflow: auto; padding: 0 0 32px; }
.is-full .modal-body .section-heading { margin: 0; padding: 22px 18px 9px; }
.is-full .notice-item { padding: 15px 18px; }
.is-full .notice-copy b { font-size: var(--t-body-lg); }
.is-full .notice-copy span { font-size: var(--t-body-md); }

/* ---- The palette page, rebuilt on the published ramps ----------------------
   Each chip is filled from the token rather than a repeated hex, so the page
   cannot drift from the system: change a variable and this page changes with
   it. The hex beside it is the published value, so a mismatch would show. */
.color-rack--system { display: grid; gap: 26px; min-height: 0; }
.ramp { display: grid; gap: 10px; }
.ramp-name { margin: 0; font-family: var(--sans); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--quiet); }
.ramp-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--doc-gap); }
.ramp-row article { display: grid; gap: 6px; align-content: start; }
.ramp-row i { display: block; height: 56px; border-radius: var(--radius-inside);
  border: 1px solid var(--line); }
.ramp-row span { font-size: 12px; font-weight: 600; }
.ramp-row code { font-family: var(--mono); font-size: 11px; color: var(--muted); }
@media (max-width: 720px) { .ramp-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.gray-rack article i { border: 1px solid var(--line); }

/* ---- Powered by Maestro, one lockup ---------------------------------------
   The client's file supplies "Powered by" and the wordmark as a single 135×42
   drawing, so the text node and the separate logo are gone — the lockup is the
   whole signature now.

   It ships inline with `currentColor` rather than as an <img>. The supplied
   file is filled #060607, which would have been invisible on the splash's black
   ground, and an <img> cannot inherit colour. Inline, it takes whatever ground
   it sits on — the same trap the white-only CSO logo still has. */
.splash-powered { display: block; color: var(--c-white); }
.powered-lockup { display: block; width: 135px; height: 42px; }

/* ---- The programme sheet is tied to the bottom ----------------------------
   It floated in the slot the programme bar had occupied, which left a gap under
   it and cost height the content needed — the About tonight paragraph was being
   cut mid-sentence. It meets the bottom edge now: full width, square to the
   frame at the foot, rounded only where it rises, and covering the tab bar the
   way a bottom sheet should. The body reserves the home handle. */
.modal-card.sheet-up {
  inset: auto 0 0 0;
  max-height: 82%;
  border-radius: var(--radius-container) var(--radius-container) 0 0;
  border-left: 0; border-right: 0; border-bottom: 0;
}
.sheet-up .modal-body { padding-bottom: calc(var(--safe-bottom) + 8px); }
/* The programme list closed with a rule and then a section heading followed —
   two separations doing one job. The last row keeps its content, not its edge. */
.sheet-up .kv-list > div:last-child { border-bottom: 0; }
/* The programme plate is a photograph of the room now, not a letter in a grey
   box. The caption keeps its legibility with a scrim rather than a fill, so the
   picture is not covered by the thing describing it. */
.program-art.has-plate { background: var(--c-black); position: relative; padding: 0; min-height: 0; }
.program-plate { display: block; width: 100%; height: 168px; object-fit: cover; opacity: .92; }
.program-art.has-plate figcaption { position: absolute; left: 0; right: 0; bottom: 0;
  padding: 34px 16px 14px; color: var(--c-white);
  background: linear-gradient(to top, rgb(6 6 7 / .82), rgb(6 6 7 / 0)); }
.program-art.has-plate em { color: rgb(255 255 255 / .82); }
.program-art.has-plate b { color: var(--c-white); }

/* ═══ CARD REGISTER ═════════════════════════════════════════════════════════
   Grouping moves from rules to cards. A hairline between rows says "these are
   separate"; a card says "these belong together", which is the stronger and
   more useful statement — and it is what the published Radius tokens were for.

   The audit sorted every border in the app into two classes:
     · dividers inside a list — removed, every one
     · edges that define a control or an object — kept
   A button, chip, field or badge still has an edge. Nothing else does.

   Cards are white on the Gray 100 ground, so they separate by value rather
   than by a drawn line, with Radius/Container and a shadow soft enough to read
   as lift rather than as decoration.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
  --card-shadow: 0 1px 2px rgb(6 6 7 / .05), 0 8px 20px rgb(6 6 7 / .05);
  --card-pad: 6px;
}

/* Every grouped list is one card. The bleed margins go with the rules — a card
   sits on the gutter, it does not run to the device edge. */
.screen .held-list,
.screen .notice-list,
.screen .kv-list,
.screen .benefit-list,
.screen .settings-list,
.screen .history-list,
.screen .friend-list,
.screen .sponsor-list,
.screen .patron-classifiers,
.screen .incl-list,
.screen .stat-row {
  background: var(--c-white);
  border-radius: var(--radius-container);
  box-shadow: var(--card-shadow);
  border: 0;
  margin-left: 0;
  margin-right: 0;
  padding-left: 0;
  padding-right: 0;
  width: 100%;
  box-sizing: border-box;
  overflow: hidden;
  gap: 0;
}

/* No rules between rows — spacing carries the separation now. */
.screen .held-list li > button,
.screen .notice-item,
.screen .kv-list > div,
.screen .benefit-list button,
.screen .settings-list button,
.screen .history-list li,
.screen .friend-list li,
.screen .sponsor-list li,
.screen .patron-classifiers li,
.screen .incl-list li,
.screen .stat-row > div,
.modal-card .notice-item,
.sheet-up .kv-list > div,
.accordion details,
.accordion summary {
  border-top: 0;
  border-bottom: 0;
}

/* Rows breathe on the card's own padding rather than against a line. */
.screen .held-list li > button { padding: 14px 16px; }
.screen .kv-list > div,
.screen .benefit-list button,
.screen .settings-list button,
.screen .history-list li,
.screen .friend-list li,
.screen .sponsor-list li { padding: 13px 16px; }
.screen .patron-classifiers li,
.screen .incl-list li { padding: 13px 16px; }
.screen .stat-row > div { padding: 14px 16px; }

/* The membership card is white on the ground like the rest, and its stat band
   is separated by space instead of a rule. */
.member-card {
  background: var(--c-white);
  border: 0;
  box-shadow: var(--card-shadow);
  padding: 18px;
}
.member-card:active { background: var(--c-white); }
.member-board { border-top: 0; margin-top: 16px; padding-top: 0; }
.member-head { border-bottom: 0; padding-bottom: 0; }

/* Objects that were drawn with an edge become cards too. */
.challenge, .plan-strip, .renew-block, .program-listen, .badge-rail li {
  background: var(--c-white);
  border: 0;
  box-shadow: var(--card-shadow);
}
.search-field { background: var(--c-white); border: 0; box-shadow: var(--card-shadow); }
.field input { background: var(--c-white); border: 0; box-shadow: var(--card-shadow); }
.field input:focus-visible, .field input.is-hover { background: var(--c-white); }

/* Sheets and panels: the header rule goes, the padding does the work. */
.modal-head, .is-full .modal-head, .sheet-up .modal-head, .drop-panel .modal-head {
  border-bottom: 0;
}
.filter-foot { border-top: 0; }
.accordion { border-top: 0; }
.settings-note { border-bottom: 0; }

/* Grid hierarchy: one rhythm between blocks, from the spacing scale. */
.screen .section-block { margin-top: var(--sp-24); }
.screen .section-heading { margin-bottom: var(--sp-8); }
.screen .sub-module + .sub-module { margin-top: var(--sp-16); }

/* ---- The last of the rules ------------------------------------------------
   Four survived the pass above on specificity, all of them list dividers:
   patron classifiers, settings rows, key/value rows and the notice rows. Taken
   out at the weight that wins, and stated as `border: 0` so no edge can come
   back through a longhand. */
.screen .patron-classifiers li,
.screen .patron-classifiers li:last-child,
.screen .settings-list button,
.screen .settings-list:not(.settings-list--single) button,
.screen .kv-list > div,
.screen .kv-list > div:last-child,
.screen .notice-item,
.screen .notice-item.is-static,
.screen .notice-item:last-child,
.modal-card .notice-item,
.modal-card .notice-item:last-child,
.is-full .notice-item,
.is-full .notice-item:last-child,
.screen .benefit-list button,
.screen .history-list li,
.screen .friend-list li,
.screen .sponsor-list li,
.screen .stat-row > div,
.screen .incl-list li { border: 0; }

/* ---- Key/value rows, after the reference ----------------------------------
   Label quiet on the left, value dark on the right, nothing drawn between them.
   The pair is legible because the two weights differ, not because a line
   separates them. */
.screen .kv-list > div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  padding: 11px 16px;
}
.screen .kv-list > div > span,
.screen .kv-list > div > dt { color: var(--c-gray-300); font-size: var(--t-body-md); }
.screen .kv-list > div > b,
.screen .kv-list > div > dd { color: var(--ink); font-weight: 650; font-size: var(--t-body-md); margin: 0; }
.screen .kv-list { padding: 6px 0; }

/* ---- Action pairs ---------------------------------------------------------
   A filled dark primary beside a filled grey secondary, equal height, sharing
   the row — the reference's pairing, and it removes the outlined secondary
   that was competing with the row buttons elsewhere. */
.renew-actions, .event-actions, .sheet-actions {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-8);
}
.renew-actions .btn, .sheet-actions .btn { width: 100%; min-height: 46px; }
.btn--secondary {
  background: var(--c-gray-100);
  border: 0;
  color: var(--ink);
}
.btn--secondary:active { background: var(--c-gray-200); }
.btn--tertiary { border: 0; }
/* A `:last-child` rule from the hairline pass was outranking the removal — it
   closed each list with one more line. Matched at the same weight. */
.screen .held-list li:last-child > button,
.screen .notice-item:last-child,
.modal-card .notice-item:last-child,
.screen .benefit-list li:last-child button { border: 0; }

/* Panels lift with shadow rather than a drawn edge, like the cards. */
.modal-card.sheet-up { border: 0; }
.modal-card.drop-panel { border: 0; box-shadow: 0 18px 44px rgb(6 6 7 / .22); }
.drop-menu { border: 0; box-shadow: 0 12px 30px rgb(6 6 7 / .18); }
.is-full .modal-head { box-shadow: none; }
/* The pass and the access sheet were still separating with rules. Same
   treatment: the group is the card, the rows sit on spacing. */
.pass-ticket-seats, .pass-ticket-package,
.choice, .choice + .choice,
.screen .choice { border: 0; }
.choice-icon { border: 0; background: var(--c-gray-100); }
.pass-ticket { box-shadow: var(--card-shadow); border: 0; }
/* `.access-sheet .choice` carried two classes and outranked the removal. The
   access options become one card with the rows on spacing, like every other
   list in the app. */
.access-sheet .choice,
.access-sheet .choice:last-of-type,
.choice:last-of-type { border: 0; }
.access-sheet .choice-list,
.sheet-panel .choice-list {
  background: var(--c-white); border-radius: var(--radius-container);
  box-shadow: var(--card-shadow); overflow: hidden; padding: 4px 0;
}
.access-sheet .choice { padding: 14px 16px; min-height: 0; }
.support-note { border: 0; background: var(--c-white); box-shadow: var(--card-shadow);
  border-radius: var(--radius-container); }
/* Rows inside the dropdown separated with rules — a line inside a card. The
   hover state and the spacing carry it. */
.drop-menu button, .drop-menu button:last-child { border: 0; }
.drop-menu { padding: 6px 0; }
.drop-menu button { min-height: 42px; }

/* ═══ CARDS: FILL ONLY ══════════════════════════════════════════════════════
   No shadow, no stroke. A card is a white block on the Gray 100 ground and
   that value difference is the whole signal — which is the same argument that
   retired the hairlines, carried one step further. Anything drawn around a card
   is a second statement of something the fill already said.
   ═══════════════════════════════════════════════════════════════════════════ */
:root { --card-shadow: none; }
.screen .held-list, .screen .notice-list, .screen .kv-list,
.screen .benefit-list, .screen .settings-list, .screen .history-list,
.screen .friend-list, .screen .sponsor-list, .screen .patron-classifiers,
.screen .incl-list, .screen .stat-row,
.member-card, .challenge, .plan-strip, .renew-block, .program-listen,
.badge-rail li, .search-field, .field input, .support-note, .pass-ticket,
.access-sheet .choice-list, .sheet-panel .choice-list,
.modal-card.drop-panel, .drop-menu, .modal-card.sheet-up {
  box-shadow: none;
  border: 0;
}

/* ---- Item lists are cards each, attribute lists are one card --------------
   A concert, a notice and a benefit are separate things that happen to be
   listed together, so each gets its own card and its own air. A set of
   key/values describes one thing, so it stays a single card. */
.screen .held-list,
.screen .notice-list,
.screen .benefit-list,
.screen .history-list,
.screen .friend-list {
  background: none;
  border-radius: 0;
  overflow: visible;
  display: grid;
  gap: var(--sp-8);
  padding: 0;
}
.screen .held-list li > button,
.screen .notice-item,
.screen .benefit-list button,
.screen .history-list li,
.screen .friend-list li {
  background: var(--c-white);
  border-radius: var(--radius-container);
  padding: 14px 16px;
}
.screen .held-list li { background: none; }

/* ---- Action pairs ---------------------------------------------------------
   "Talk to the box office" was running out of its column, underlined, and
   clipped at the card edge. Equal columns that wrap rather than overflow, no
   underline, and the secondary is a filled grey block rather than an outline
   competing with the primary beside it. */
.renew-actions, .sheet-actions, .event-actions {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-8); width: 100%;
}
.renew-actions .btn, .sheet-actions .btn, .event-actions button {
  width: 100%; min-height: 46px; padding: 0 12px;
  white-space: normal; line-height: 1.15; text-align: center;
  text-decoration: none; letter-spacing: .05em;
}
.btn--tertiary { background: var(--c-gray-100); color: var(--ink); text-decoration: none; }
.btn--tertiary:active { background: var(--c-gray-200); }

/* ---- The hero action is a real primary -----------------------------------
   It was a pale grey block sitting on a photograph, which read as disabled. */
.hero-cta {
  background: var(--c-white);
  color: var(--ink);
  border-radius: var(--radius-button);
  min-height: 46px;
  padding: 0 20px;
  font-size: var(--t-body-md);
  font-weight: 650;
}
.hero-cta:active { background: var(--c-gray-100); }

/* ---- Radii on the published tokens ----------------------------------------
   Enumerated every rendered corner in the app: three values were off the
   system — 14px on the rail cards, journey entries and a few rows, and 22px on
   the sheet panels. Neither is a token. Snapped to Radius/Container 16 for
   anything that is a card, Radius/Inside 8 for anything sitting within one.

   The allowed set is exactly: 0 · 8 · 16 · 99 · 999 · 50%. Nothing else. */
.announce, .journey-entry, .sheet-panel,
.member-rail > *, .renew-block, .plan-strip,
.pass-ticket, .modal-card, .drop-menu,
.access-sheet .choice-list, .sheet-panel .choice-list {
  border-radius: var(--radius-container);
}
.sheet-panel { border-radius: var(--radius-container) var(--radius-container) 0 0; }
.modal-card.sheet-up { border-radius: var(--radius-container) var(--radius-container) 0 0; }
.modal-card.is-full { border-radius: 0; }
/* Anything nested inside a card takes the inside step, not the container one. */
.screen .held-thumb, .journey-thumb, .announce-thumb,
.pass-code-zone, .choice-icon, .badge-ico, .program-plate,
.friend-ini, .patron-portrait { border-radius: var(--radius-inside); }
.friend-ini, .round-btn, .round-action { border-radius: 50%; }

/* The last two off-token corners: the household grid and the settings rows. */
.screen .incl-list li,
.screen .settings-list button { border-radius: var(--radius-inside); }

/* ---- Typographic hierarchy, after the reference ---------------------------
   Section headings were 12px uppercase with .08em tracking — a label, set as
   quietly as the counts beside them, so the page had no middle voice between
   the screen title and the body. They are sentence-case and bold now, at
   Subheading MD, which is what the reference uses and what the published type
   scale already had a step for. Uppercase stays where it is genuinely a tag:
   the card kickers that classify a thing (SUBSCRIPTION, MEMBERSHIP).

   The trailing count is a quiet companion to the heading rather than a second
   label competing with it. */
.screen .section-heading h2,
.modal-body .section-heading h2 {
  font-family: var(--sans);
  font-size: var(--t-subheading-md);
  line-height: var(--t-subheading-md-lh);
  font-weight: 650;
  letter-spacing: -.012em;
  text-transform: none;
  color: var(--ink);
}
.screen .section-heading span,
.screen .section-heading button,
.modal-body .section-heading span {
  font-size: var(--t-body-md);
  letter-spacing: 0;
  text-transform: none;
  color: var(--c-gray-300);
  font-weight: 500;
}
/* Body copy gets the reading leading the reference uses. */
.challenge p, .sponsor-list small, .program-about,
.renew-block p, .support-note, .settings-note {
  line-height: 1.55;
}

/* ---- The programme capsule carries a thumbnail ----------------------------
   After the reference: a rounded image, the title and its subtitle, and the
   action. The picture comes from the same venue map as the programme sheet, so
   the capsule and the sheet it opens always show the same room.

   A pill cannot hold a thumbnail without the image fighting the curve, so the
   capsule takes Radius/Container and the image Radius/Inside — the pairing the
   token set was built for. The live dot moves onto the title, where it reads as
   a state on the words rather than a bullet floating beside a picture. */
.program-bar {
  border-radius: var(--radius-container);
  padding: 10px 12px 10px 10px;
  gap: 12px;
}
.program-thumb {
  width: 44px; height: 44px; flex: none;
  border-radius: var(--radius-inside);
  object-fit: cover;
  background: var(--c-gray-500);
}
.program-live { gap: 12px; align-items: center; }
.program-copy { display: grid; gap: 2px; min-width: 0; }
.program-copy b { display: flex; align-items: center; gap: 7px; }
.program-copy small { display: block; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; }
.live-dot { width: 6px; height: 6px; flex: none; }

/* ---- The capsule, larger ---------------------------------------------------
   The thumbnail was 44px against two lines of text and read as an afterthought.
   56px gives the picture the same weight as the words it belongs to. */
.program-bar { padding: 10px 10px 10px 10px; gap: 14px; }
.program-thumb { width: 56px; height: 56px; }
.program-copy b { font-size: 15px; }
.program-copy small { font-size: 13px; }

/* ---- The capsule action becomes an affordance -----------------------------
   "PROGRAM ^" was a tertiary button doing a job tertiary is wrong for: it sat
   at the same volume as the title beside it and competed with the one thing the
   capsule exists to say. The control is a chevron in a quiet disc — the whole
   capsule reads as one object with one obvious way to open it. */
.program-open {
  width: 36px; height: 36px; flex: none;
  border-radius: 50%;
  background: rgb(255 255 255 / .14);
  color: var(--c-white);
  display: grid; place-items: center;
  padding: 0; min-height: 0;
  letter-spacing: 0; font-size: 0;
}
.program-open .icon { width: 15px; height: 15px; }
.program-open:active { background: rgb(255 255 255 / .24); }

/* ---- Tertiary finds its job ------------------------------------------------
   It had none: the box-office action became a filled grey block, so the style
   was defined and unused. Tertiary is the quiet, inline, text-level action that
   sits *inside* something else and must not compete with it — "View all" on a
   rail heading, "Clear all" beside the applied filters, "Show less". Those were
   each hand-styled one-offs; they are one component now. */
.rail-more, .clear-all, .linkish.pass-connect {
  background: none;
  border: 0;
  border-radius: var(--radius-button);
  color: var(--brand);
  font: inherit;
  font-size: var(--t-body-md);
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  text-decoration: none;
  cursor: pointer;
  padding: 6px 8px;
  margin-right: -8px;
  display: inline-flex; align-items: center; gap: 4px;
  min-height: 36px;
}
.rail-more:active, .clear-all:active, .linkish.pass-connect:active { background: var(--c-gray-100); }
.rail-more .icon { width: 14px; height: 14px; }

/* ---- The colour page, on a grid -------------------------------------------
   The system rack was inheriting `grid-template-columns: 1fr 1fr` from the old
   six-swatch rack, so five ramps were being flowed into columns and every chip
   was crushed — names and hexes overlapping each other.

   One ramp per row, five swatches across, each a wide block with its name and
   value set beneath it in the mono face. Same treatment for the grayscale rack
   so the two sections read as one system rather than two layouts. */
.color-rack--system {
  display: grid;
  grid-auto-flow: row;
  grid-template-columns: 1fr;
  gap: var(--sp-32);
  min-height: 0;
}
.ramp { display: grid; gap: 12px; }
.ramp-name {
  margin: 0;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--quiet);
}
.ramp-row {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 0;
}
.ramp-row article {
  display: grid;
  gap: 0;
  align-content: start;
  padding: 0;
  border: 0;
  min-height: 0;
}
.ramp-row i {
  display: block;
  height: 104px;
  border: 0;
  border-radius: 0;
}
.ramp-row span {
  margin-top: 10px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--ink);
}
.ramp-row code {
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--quiet);
}

/* Grayscale, same grid. Five values, five columns — the six-column template
   left a dead track at the end. */
.gray-rack { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 0; }
.gray-rack i { height: 104px; border: 0; }
.gray-rack article i { border: 0; }
.gray-rack code { margin-top: 10px; font-family: var(--mono); font-size: 11px; color: var(--ink); }
.gray-rack span { margin-top: 3px; font-family: var(--mono); font-size: 11px; color: var(--quiet); }
/* The lightest chip needs an edge or it disappears into the page. */
.ramp-row article:first-child i[style*="--c-white"],
.gray-rack article:first-child i { box-shadow: inset 0 0 0 1px var(--c-gray-200); }
/* The chips were 45px inside 168px tracks: `.color-rack article` is a flex
   column from the old rack, so the swatch sat at its content width instead of
   filling. Stretch them. */
.ramp-row article, .gray-rack article {
  display: grid;
  justify-items: stretch;
  align-items: start;
  padding: 0;
}
.ramp-row i, .gray-rack i { width: 100%; }
.ramp-row article + article, .gray-rack article + article { margin-left: 0; }
/* The article is a grid whose single column was auto-sized to its widest text,
   so `width: 100%` on the swatch resolved against ~45px rather than the 168px
   track. A 1fr column makes the area the full track, and the swatch fills it. */
.ramp-row article, .gray-rack article { grid-template-columns: minmax(0, 1fr); }

/* ---- Identity sections align on the baseline, not the box -----------------
   The label column and the content column already started at the same y — the
   boxes were flush. What did not line up was the type: a 19px bold heading and
   a 16px note have different first baselines inside identical boxes, so the
   two columns read as misaligned even though their tops matched.

   Grid baseline alignment puts the heading's first baseline on the content's
   first baseline, which is the line the eye actually follows. The section
   number joins the same line rather than floating above the word. */
.identity-section { align-items: baseline; }
.identity-section header {
  display: flex;
  align-items: baseline;
  gap: 14px;
}
.identity-section header span {
  flex: none;
  line-height: inherit;
}
/* A section whose content opens with a rack rather than a line of type has no
   baseline to meet, so it keeps the top edge. */
.identity-section.color-system,
.identity-section.gray-system,
.identity-section.mark-system { align-items: start; }

/* ---- Baseline alignment, done without breaking the rows -------------------
   Grid `align-items: baseline` on the section aligned the two columns, but it
   also stopped the body column stretching, so every principles article was
   sized to one row height and the longer paragraphs ran out past their box —
   "Grouping" landed on the last line of "Type". Alignment is not worth a
   collision.

   Back to top alignment, which was already flush to the pixel, and the optical
   difference is closed where it is actually visible: a 19px bold heading beside
   an 11px uppercase rack label. The label takes the offset, not the grid. */
.identity-section { align-items: start; }
.color-system .ramp:first-child .ramp-name,
.gray-system .gray-rack { padding-top: 6px; }

/* The old six-swatch rack drew a rule down the right of every article; on the
   ramps that left a stray line hanging past the last swatch. */
.ramp-row article, .gray-rack article,
.color-rack--system article { border: 0; }
.color-rack--system article:last-child { border-right: 0; }
/* Eight pixels between two blocks of prose is not a gap, it is a collision
   waiting for a longer paragraph — which is what it became. The principles
   articles sit on the spacing scale like everything else. */
.principles > div,
.identity-section.principles > div { row-gap: var(--sp-32); column-gap: var(--sp-40); }
/* The class is .identity-principles, not .principles. */
.identity-principles > div { row-gap: var(--sp-32); column-gap: var(--sp-40); }

/* ---- Section descriptions fill their column -------------------------------
   The identity section intros carried a 46ch measure, which left most of a
   1077px column empty beside two or three lines of text. In a reference
   document the description should occupy the column it is given. */
.identity-section .token-note,
.photo-lede,
.map-note { max-width: none; }

/* ---- Mobile spacing audit -------------------------------------------------
   Measured at 390px across all six screens: the gap between a section heading
   and the card beneath it was 8px on almost every block, which reads as the
   label touching the content. Sections themselves were already on 24px.

   Headings and their trailing count sat on one line and collided once a
   heading wrapped, which the longer labels now do. The count wraps to its own
   line at narrow widths and aligns left with the heading instead of being
   pushed against the edge. */
.screen .section-heading { margin-bottom: 12px; row-gap: 2px; flex-wrap: wrap; }
.screen .section-heading h2 { flex: 1 1 auto; min-width: 0; }
.screen .section-heading > span,
.screen .section-heading > button { flex: 0 0 auto; }
.screen .event-group-head { flex-wrap: wrap; row-gap: 2px; margin-bottom: 12px; }
.screen .event-group-head .event-group-name { flex: 1 1 auto; min-width: 0; }
.screen .event-group-head .event-group-kind { flex: 0 0 auto; }
/* A wrapped heading should not sit tighter than an unwrapped one. */
.screen .sub-module + .sub-module { margin-top: var(--sp-24); }

/* ---- Content model table --------------------------------------------------
   A reference table in About, listing each repeating record, the class it maps
   to, and the fields a CMS would supply. It scrolls in its own container rather
   than forcing the page sideways at narrow widths. */
.cms-table { overflow-x: auto; margin: 18px 0 22px; -webkit-overflow-scrolling: touch; }
.cms-table table { width: 100%; min-width: 720px; border-collapse: collapse; }
.cms-table th {
  text-align: left; padding: 0 14px 10px 0;
  font-family: var(--sans); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--quiet); font-weight: 500;
}
.cms-table td { padding: 12px 14px 12px 0; vertical-align: top;
  font-size: 13.5px; line-height: 1.45; color: var(--muted); }
.cms-table td:first-child { color: var(--ink); white-space: nowrap; }
.cms-table tbody tr { background: var(--c-white); }
.cms-table tbody td:first-child { border-radius: var(--radius-inside) 0 0 var(--radius-inside); padding-left: 14px; }
.cms-table tbody td:last-child { border-radius: 0 var(--radius-inside) var(--radius-inside) 0; }
.cms-table tbody tr + tr td { box-shadow: 0 -6px 0 var(--surface); }
.cms-table code { font-family: var(--mono); font-size: 12px; color: var(--ink); }

/* ---- Event detail ---------------------------------------------------------
   The hero was a placeholder: a dark plate with the concert's initial. It takes
   the venue photograph now, from the same map the program sheet and the now
   playing capsule read, so the three always show the same hall. */
.event-hero { position: relative; margin: 0; overflow: hidden; background: var(--c-black); }
.event-hero-img { display: block; width: 100%; height: 212px; object-fit: cover; opacity: .92; }
.event-hero .event-id {
  position: absolute; left: 16px; top: 14px; z-index: 2;
  color: var(--c-white); font-family: var(--mono); font-size: 11px;
  text-shadow: 0 1px 6px rgb(6 6 7 / .6);
}
.event-hero .feature-letter { display: none; }

/* Three actions on one row at 390px cannot hold uppercase labels without
   wrapping, which is what pushed two of them onto a second line. Sentence case,
   equal columns, and the primary carries the fill. */
.inline-actions {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-8);
  margin: 16px 0 4px;
}
.inline-actions .btn:first-child { grid-column: 1 / -1; }
.inline-actions .btn {
  width: 100%; min-height: 46px; padding: 0 12px;
  text-transform: none; letter-spacing: 0;
  font-size: var(--t-body-md); font-weight: 650;
  white-space: nowrap;
}
.attended-trigger {
  width: 100%; min-height: 46px; margin-top: 18px;
  text-transform: none; letter-spacing: 0;
  font-size: var(--t-body-md); font-weight: 650;
}

/* The program list is a card, like every other list in the app. */
.program { margin-top: 22px; }
.program h2 { font-size: var(--t-subheading-md); font-weight: 650; letter-spacing: -.012em;
  margin: 0 0 12px; }
.program > div {
  display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
  background: var(--c-white); padding: 13px 16px; border: 0;
}
.program > div:first-of-type { border-radius: var(--radius-container) var(--radius-container) 0 0; }
.program > div:last-of-type { border-radius: 0 0 var(--radius-container) var(--radius-container); }
.program > div span { color: var(--c-gray-300); font-size: var(--t-body-md); }
.program > div strong { color: var(--ink); font-size: var(--t-body-md); font-weight: 650; }
/* The event program list is a card; the rule above it and the rules between its
   rows were left from the divider era. */
.program { border-top: 0; padding-top: 0; }
.program div { border-bottom: 0; }
/* Secondary keeps its fill here: .inline-actions was resetting it to nothing. */
.inline-actions .btn--secondary { background: var(--c-gray-100); color: var(--ink); border: 0; }
.inline-actions .btn--secondary:active { background: var(--c-gray-200); }
.inline-actions .btn--primary { background: var(--brand); color: var(--c-white); }

/* ---- Header mark and bell -------------------------------------------------
   The two are already centred on the same line: measured at 4x, the logo's
   centre and the bell icon's centre both land on the same pixel. What made the
   bell look higher was its unread dot, which sat outside the disc with a ring
   around it and pulled the whole control's visual mass upward.

   The dot moves onto the disc edge, and the mark comes up from 15px to 18px so
   it carries comparable weight beside a 40px control. */
/* On a 40px disc, a 4px inset from the square box still lands outside the
   circle. ~7px puts the dot on the edge instead of floating past it. */
.round-btn.has-unread::before { top: 7px; right: 7px; width: 7px; height: 7px; box-shadow: none; }
.mark-pill .cso-logo { height: 18px; }
/* The Powered by Maestro lockup is one <symbol> in the document defs, filled
   with currentColor. The supplied file is #060607, which would be invisible on
   the splash, and a <use> reference inherits color where an <img> cannot. */
.splash-powered { display: block; margin: 0; color: var(--c-white); }
.powered-lockup { display: block; width: 135px; height: 42px; }

/* The phone is a touch surface. A hover fill on a list row lights one card up
   under the cursor while its siblings stay flat, which reads as a selected
   state that is not real — it showed as a white card on the October events
   list. Every tappable row and card above uses :active instead, which a finger
   can actually produce. Do not reintroduce :hover inside .phone. */

/* ── BUTTONS: BLACK PRIMARY, TEXT TERTIARY ─────────────────────────────────
   The patron app is CSO's, not Maestro's, so the filled action is the black
   from the brand ramp rather than the burgundy. Tertiary carries no surface
   at all — it is a text action, so a fill or a stroke on it makes three filled
   buttons where the hierarchy needs one. Placed last because earlier blocks
   set --brand on primary and a gray fill on tertiary in five places. */
.btn--primary,
.inline-actions .btn--primary {
  background: var(--c-black); color: var(--c-white); border: 0;
}
.btn--primary:active, .btn--primary.is-hover,
.inline-actions .btn--primary:active { background: var(--c-gray-500); border: 0; }
.btn--primary:active, .btn--primary.is-active { background: var(--c-gray-500); }

.btn--tertiary,
.btn--tertiary:active, .btn--tertiary.is-hover,
.btn--tertiary:active, .btn--tertiary.is-active,
.own-actions .btn--tertiary,
.renew-actions .btn--tertiary {
  background: transparent; border: 0; box-shadow: none;
  color: var(--c-black); padding: 0 var(--sp-8);
}
.btn--tertiary:active, .btn--tertiary.is-active { color: var(--c-gray-500); }

/* ── DROP MENU ─────────────────────────────────────────────────────────────
   Anchored under its chip by app.js. Without separation it read as a loose
   flap of paper laid on the list — the month heading behind it poked out at
   the left edge and the cards ran straight into its side. A shadow is not
   available here (the register has none), so the scrim does the separating:
   it dims what the menu covers, which is also what tells you the rest of the
   screen is inert until you choose. Every value below is a token. */
.stage-over--menu .menu-scrim { background: rgb(6 6 7 / .32); }
.drop-menu {
  background: var(--c-white);
  border-radius: var(--radius-container);
  padding: var(--sp-8) 0;
  min-width: 184px; max-width: 224px;
  overflow: hidden;
}
.drop-menu button {
  min-height: 44px;
  padding: 0 var(--sp-16);
  font-size: var(--t-body-md); line-height: var(--t-body-md-lh);
  color: var(--c-black);
}
.drop-menu button.is-on { font-weight: 650; }
.drop-menu button:active { background: var(--c-gray-100); }
/* The Uniqlo register squared every .btn, which left the app's buttons at 0
   while the design system page documented them at --radius-button. One value,
   from the token. */
.btn, .hero-cta,
.pass-ticket .btn, .renew-actions .btn, .sheet-actions .btn,
.primary-action, .secondary-action {
  border-radius: var(--radius-button);
}

/* ── CONTENT MODEL AS AN ABOUT SUBPAGE ─────────────────────────────────────
   Twelve records across five columns needed 720px of table and ran off both
   edges of the page. The columns were never comparable anyway — one holds a
   class name, three hold sentences — so each record becomes a card carrying
   its own labelled fields, on the same measure as the rest of About. */
#about-content-model .cms-records {
  list-style: none;
  margin: var(--sp-24) 0 var(--sp-16);
  padding: 0;
  display: grid;
  gap: var(--sp-16);
}
.cms-record {
  background: var(--c-white);
  border-radius: var(--radius-container);
  padding: var(--sp-16);
}
.cms-record-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-8) var(--sp-16);
  margin-bottom: var(--sp-16);
}
#about-content-model .cms-record-head h3 {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--t-body-lg); line-height: var(--t-body-lg-lh);
  font-weight: 650;
  letter-spacing: 0; text-transform: none; color: var(--c-black);
}
.cms-record-head code {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 13px;
  color: var(--c-gray-500);
}
.cms-record-fields { margin: 0; display: grid; gap: var(--sp-8); }
/* Label and value share a row on a wide page and stack on a narrow one; the
   label column is fixed so the values line up down the whole list. */
.cms-record-fields > div {
  display: grid; grid-template-columns: 116px minmax(0, 1fr); gap: var(--sp-16);
}
.cms-record-fields dt {
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--c-gray-500);
  padding-top: 2px;
}
.cms-record-fields dd {
  margin: 0;
  font-size: 15px; line-height: 1.5;
  color: var(--c-black);
}
@media (max-width: 560px) {
  .cms-record-fields > div { grid-template-columns: minmax(0, 1fr); gap: 2px; }
}

/* ── CLASSIFIERS AS RECORDS ────────────────────────────────────────────────
   A classifier was a label and a category in gray caps: it named the thing
   the patron holds but said nothing about it, and it went nowhere. Each one
   is now a record — icon plate, what it is, where it stands, and the screen
   it opens — with the standing counted rather than described, because the
   counts already exist elsewhere on this screen (2 of 6 attended, one of
   three benefits used, two in the household) and a count is checkable where
   a percentage is not. Three columns, not four: at 390px a fourth column
   squeezed the title into two lines, so the kind moves to an eyebrow above
   the title and the right side carries only the meter and the chevron. */
.screen .patron-classifiers {
  list-style: none; margin: var(--sp-16) 0 0; padding: 0;
  display: grid; gap: var(--sp-8);
  background: none;
}
.screen .patron-classifiers li { display: block; padding: 0; border: 0; background: none; }
.screen .patron-classifiers li > button {
  width: 100%;
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr) auto 14px;
  align-items: center;
  gap: 12px;
  padding: 14px var(--sp-16);
  border: 0;
  border-radius: var(--radius-container);
  background: var(--c-white);
  text-align: left;
  font: inherit;
  cursor: pointer;
}
.screen .patron-classifiers li > button:active { background: var(--c-gray-100); }
.classifier-ico {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: var(--radius-inside);
  background: var(--c-gray-100);
  color: var(--c-black);
}
.classifier-ico .icon { width: 20px; height: 20px; }
.classifier-copy { min-width: 0; display: grid; gap: 1px; }
.classifier-copy b {
  font-family: var(--sans);
  font-size: 15px; line-height: 1.2;
  font-weight: 650; color: var(--c-black);
}
.classifier-copy small {
  font-family: var(--sans); font-size: 12.5px; line-height: 1.25;
  color: var(--c-gray-500);
}
.screen .patron-classifiers .classifier-kind {
  font-style: normal;
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-gray-500);
}
.screen .patron-classifiers .icon-chevron { width: 14px; height: 14px; color: var(--c-gray-300); }
/* The meter is a count, one segment per unit, so it reads as "two of six"
   rather than as a proportion nobody can check. */
.classifier-meter { display: flex; align-items: center; gap: 3px; }
.classifier-meter i {
  display: block; width: 8px; height: 3px;
  border-radius: var(--radius-round);
  background: var(--c-gray-200);
}
.classifier-meter i.is-on { background: var(--c-black); }

/* ── COMPONENT PREVIEWS ────────────────────────────────────────────────────
   Design system only: About says what a record holds, the design system shows
   what it looks like. A preview is the real component, cloned and scaled, so
   it cannot drift from what ships. The stage keeps the prototype's 358px content width and is
   scaled by transform; a transform does not affect layout, so the slot has to
   reserve the scaled height itself — --thumb-scale carries the ratio and the
   slot measures the stage through it. */
.record-cat-shot {
  margin: 0 0 var(--sp-16);
  padding: 12px;
  box-sizing: content-box;
  border-radius: var(--radius-inside);
  background: var(--c-gray-100);
  overflow: hidden;
  display: flow-root;
}
/* The stage carries .screen so component rules written `.screen .thing` still
   match, then cancels the screen's own box — a screen is a page, not a frame
   for one component. Its height is set by script after layout, because a
   transform scales what you see and not the space it occupies. */
.cmp-stage.screen {
  display: block;
  transform: none;
  padding: 0; margin: 0; min-height: 0; height: auto;
  background: none;
  transform-origin: top left;
  pointer-events: none;
}
.cmp-stage > * { margin-top: 0 !important; }


/* The design system catalogue: preview above, name and class below. */
.record-cat {
  list-style: none; margin: var(--sp-24) 0 0; padding: 0;
  display: grid; gap: var(--sp-24);
  grid-template-columns: repeat(auto-fill, minmax(256px, 1fr));
}
.record-cat > li {
  background: var(--c-white);
  border-radius: var(--radius-container);
  padding: var(--sp-16);
}
.record-cat-shot { max-width: 232px; margin-bottom: 14px; }
.record-cat-meta { display: grid; gap: 3px; }
.record-cat-meta b {
  font-family: var(--sans); font-size: 15px; line-height: 1.2;
  font-weight: 650; color: var(--c-black);
}
.record-cat-meta code {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 12px; color: var(--c-gray-500);
  word-break: break-word;
}
.record-cat-meta small {
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-gray-300);
}

/* The back link on Manage account: the colour already marks it, the rule under
   it only added weight to a control that sits above the page title. */
.screen .date .linkish { text-decoration: none; }

/* ── TAB BAR CONTRAST ──────────────────────────────────────────────────────
   The bar was burgundy on gray-400, which put both states in the middle of
   the ramp: the active tab did not read as much darker, only as coloured, and
   Gray 400 is under AA at 10.5px. Active is the ink; the rest are the same
   ink at half alpha, so the difference is weight rather than hue and the
   inactive labels sit on the same axis as everything else. */
.bottom-nav button.is-active { color: var(--c-black); }
.bottom-nav button:not(.is-active) { color: rgb(6 6 7 / .5); }

/* ── PRIMARY BUTTON, INCLUDING ON DARK ─────────────────────────────────────
   One filled button: black ground, white uppercase label, --radius-button.
   A black button on a black ground is invisible, and this app puts primaries
   on four dark surfaces (splash, marketing, seat price, the full pass). So
   the fill is a pair of variables rather than a fixed colour, and a dark
   container swaps the pair once for everything inside it. Set --btn-fill and
   --btn-ink on any new dark surface instead of restyling the button there. */
.btn--primary,
.inline-actions .btn--primary,
.mkt-cta {
  background: var(--btn-fill, var(--c-black));
  color: var(--btn-ink, var(--c-white));
  border: 0;
  border-radius: var(--radius-button);
  text-transform: uppercase;
  letter-spacing: .085em;
  font-weight: 650;
}
.btn--primary:active, .btn--primary.is-hover,
.inline-actions .btn--primary:hover, .mkt-cta:hover {
  background: var(--btn-fill-hover, var(--c-gray-500));
  color: var(--btn-ink, var(--c-white));
}
.btn--primary:active, .btn--primary.is-active {
  background: var(--btn-fill-hover, var(--c-gray-500));
}

.splash-screen, .mkt-screen, .seat-price, .event-hero,
.program-art.has-plate, .on-dark {
  --btn-fill: var(--c-white);
  --btn-ink: var(--c-black);
  --btn-fill-hover: var(--c-gray-200);
}
/* .mkt-platform is the one light section inside the dark marketing screen, so
   it puts the pair back rather than letting its button invert on white. */
.mkt-platform {
  --btn-fill: var(--c-black);
  --btn-ink: var(--c-white);
  --btn-fill-hover: var(--c-gray-500);
}
/* A cropped preview says so, rather than ending on an arbitrary cut. */
.record-cat-shot.is-cropped { position: relative; }
.record-cat-shot.is-cropped::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 40px;
  background: linear-gradient(rgb(246 245 243 / 0), var(--c-gray-100));
  pointer-events: none;
}

/* ── EVENTS HEADER ─────────────────────────────────────────────────────────
   Title, scope switch, filter chips and the first group heading were stacked
   on 8-10px gaps, so four different kinds of control read as one block. Each
   step gets the token above it. */
.screen[data-screen="events"] .segments { margin-top: var(--sp-24); }
.screen[data-screen="events"] .filter-row { margin-top: var(--sp-16); }
.screen[data-screen="events"] .event-groups { margin-top: var(--sp-24); }
/* The row actions live inside the copy column, so "Add to calendar" ran past
   the card edge and lost its last letters. They cannot take a grid area of the
   outer row — they are not its child — so they wrap instead of clipping. */
.screen .row-actions { flex-wrap: wrap; margin-top: 10px; }

/* ── ONE LOCKUP ────────────────────────────────────────────────────────────
   The mark is the Powered by Maestro lockup everywhere — the wordmark-only
   version is gone, symbol and all. It is 135x42 where the old one was
   1025x132, so a height that suited a 7.8:1 strip makes a 3.2:1 stacked
   lockup too wide and its "Powered by" line unreadable. Each placement is
   re-set from the height at which the smaller line still holds. */
.prototype-mark .lockup,
.nav-pill .prototype-mark .lockup { height: 26px; }
.identity-intro h1 .lockup { height: 64px; }
.mark-plate .lockup { height: 48px; }
.mkt-mark .lockup { height: 26px; }
.app-header > .lockup { height: 20px; margin-top: 4px; }
.app-bar .lockup { height: 22px; }
.mark-pill .lockup { height: 20px; }
.pass-top .lockup { height: 18px; }
.patron-badge .lockup { height: 18px; }
.splash-stack .lockup { height: 42px; }
/* Tiles stretch to the row so their meta blocks land on one line: the preview
   sits at the top, the name/class/appears-on group is pushed to the bottom by
   the auto margin. A 4px gutter reads the twelve as one specimen sheet rather
   than twelve loose cards. */
.record-cat { gap: 4px; align-items: stretch; }
.record-cat > li { display: flex; flex-direction: column; }
.record-cat-meta { margin-top: auto; }
/* Tighter corners on the specimen sheet: at a 4px gutter the container radius
   read as twelve rounded cards floating apart. --radius-inside is the step the
   system already uses for anything nested inside a card. */
.record-cat > li, .cms-record { border-radius: var(--radius-inside); }
.record-cat-shot { border-radius: 4px; }
/* Some components only exist inside a closed overlay — the attribute pair in
   the programme sheet, the notice in the announcements modal. Their wrappers
   carry the closed state (offscreen, transparent, clipped), which hid the
   clone while leaving its text in the DOM. The shell keeps the class, because
   that is what the component's own rules select on, and drops the state. */
.cmp-stage .cmp-shell {
  /* !important because the states being cancelled are written on two-class
     selectors (.modal-card.sheet-up is absolutely positioned, which collapsed
     the stage to zero height and left an empty plate). A shell exists only to
     satisfy descendant selectors, so nothing here can cost a real screen. */
  position: static !important; inset: auto !important; transform: none !important;
  opacity: 1 !important; visibility: visible !important;
  height: auto !important; max-height: none !important; min-height: 0 !important;
  overflow: visible !important;
  margin: 0 !important; padding: 0 !important; border: 0 !important;
  background: none !important; box-shadow: none !important;
  backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
}
/* A component pinned to the phone frame (the now playing bar) is absolutely
   positioned, which gives it no height of its own in a preview. Only its
   placement is cancelled — its padding, fill and radius are the component. */
.cmp-stage .cmp-root {
  position: static !important; inset: auto !important;
  transform: none !important; margin: 0 !important;
}

/* ── PRIMARY: BLACK, REGULAR WEIGHT ────────────────────────────────────────
   The filled action is black wherever it lands, including on the hero
   photograph — the earlier inversion turned it white there, which read as a
   different button. The label drops to regular; at 11px uppercase with the
   letterspacing already on it, 650 was doing the work twice.
   .mkt-screen keeps the inverted pair: that ground is solid --off-black, where
   a black button is not low contrast, it is invisible. */
.event-hero { --btn-fill: var(--c-black); --btn-ink: var(--c-white); --btn-fill-hover: var(--c-gray-500); }
.btn { font-weight: 400; }
.btn--primary, .inline-actions .btn--primary, .mkt-cta { font-weight: 400; }
/* The hero action was its own component — white fill, 650, sentence case —
   which made two primaries that look nothing alike. It joins the one filled
   button: same fill variables, same uppercase label, regular weight. It keeps
   its larger 46px box, because that is its role on the hero, not a second
   style. */
.hero-cta {
  background: var(--btn-fill, var(--c-black));
  color: var(--btn-ink, var(--c-white));
  text-transform: uppercase;
  letter-spacing: .085em;
  font-size: 11px;
  font-weight: 400;
}
.hero-cta:active { background: var(--btn-fill-hover, var(--c-gray-500)); }

/* ── THIS SEASON ───────────────────────────────────────────────────────────
   Three cells auto-sized to their text, so "Years giving" wrapped while the
   others did not and the three numbers sat on three different baselines.
   Equal columns, and the label reserves two lines whether it needs them or
   not, which puts every figure on one line. */
.screen .stat-row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.screen .stat-row > div { align-content: start; min-width: 0; }
.screen .stat-row dt { min-height: 2.4em; }
.screen .stat-row dd { display: flex; align-items: baseline; gap: 5px; flex-wrap: wrap; }

/* ── RENEWAL ACTIONS ───────────────────────────────────────────────────────
   Two equal columns forced "Contact the box office" onto two centred lines
   beside a one-line primary; side by side there is no width at which both fit
   on a 390px screen. They stack instead, each sized to its own label and
   aligned with the paragraph above them. */
.renew-actions {
  grid-template-columns: minmax(0, 1fr);
  justify-items: start;
  gap: var(--sp-16);
}
.renew-actions .btn { width: auto; }
.renew-actions .btn--tertiary { min-height: 0; text-align: left; padding: 0; white-space: nowrap; }

/* ── THE JOURNEY ───────────────────────────────────────────────────────────
   Tonight was marked with a burgundy rail down the left of the row, which
   read as a stray line rather than a state. The eyebrow already says TONIGHT
   in the accent; that is the signal. Entries already attended drop to half
   alpha, so the row you are on is the only one at full strength. */
.screen .journey .journey-entry { border-left: 0; padding-left: 0; }
.screen .journey .is-now { border-left-color: transparent; }
.screen .journey .is-done { opacity: .5; }
.screen .journey .is-done .journey-thumb { opacity: 1; }

/* ── PEOPLE, AND THE ACTIONS UNDER THEM ────────────────────────────────────
   Three problems in one block. The friend cards were the container radius at
   card padding, which made two short rows look like two panels; the initials
   were a circle where every other plate in the app is a rounded square; and
   "Connect a member" carried a stroke the register removed everywhere else,
   while "Manage account" sat on no card at all beside them.

   One shape for a person row, the inside radius like the classifiers above,
   and the two actions read as what they are: connecting someone is a text
   action, opening account settings is a row like any other row. */
.screen .friend-list li,
.screen .incl-list li,
.screen .settings-list button {
  background: var(--c-white);
  border-radius: var(--radius-inside);
  padding: 12px 14px;
  border: 0;
}
.screen .friend-list, .screen .incl-list, .screen .settings-list {
  display: grid; gap: 4px; background: none; padding: 0; border-radius: 0;
}
.friend-ini {
  width: 36px; height: 36px;
  border-radius: var(--radius-inside);
  background: var(--c-gray-100);
  color: var(--c-gray-500);
}
.connect-btn {
  border: 0; background: none; padding: 0;
  min-height: 40px;
  color: var(--c-black);
  text-transform: uppercase; letter-spacing: .085em;
  font-size: 11px; font-weight: 400;
  justify-content: flex-start;
}
.connect-btn:active { background: none; color: var(--c-gray-500); }

/* ── THE SHEET, ON THE APP'S RHYTHM ────────────────────────────────────────
   The spacing rules that shape a screen are all written `.screen .thing`, and
   a sheet is not a screen — so its sections fell back to the base 24px while
   everything behind it stepped on the token scale. The gaps read as arbitrary
   next to the app. The sheet body takes the same rhythm, and its last line
   clears the home indicator instead of stopping at the frame edge, which is
   what made it look cut off. */
.modal-body .section-block { margin-top: var(--sp-32); }
.modal-body .section-block:first-child { margin-top: var(--sp-24); }
.modal-body .section-heading { margin-bottom: var(--sp-8); }
.modal-body .section-heading h2 {
  font-family: var(--sans);
  font-size: var(--t-body-lg); line-height: var(--t-body-lg-lh);
  font-weight: 650; letter-spacing: 0; color: var(--c-black);
}
.modal-body .kv-list { display: grid; gap: var(--sp-8); }
.modal-body .kv-list > div { padding: 0; border: 0; }
.modal-body .program-listen { margin-top: var(--sp-24); }
.sheet-up .modal-body { padding-bottom: calc(var(--safe-bottom) + var(--sp-24)); }
/* The programme is a fixed, short document — two works and a paragraph. At 82%
   it missed fitting by about sixty pixels, so the last line was cut mid-word
   with no scroll affordance to explain it. The sheet takes the height its
   content needs; it still scrolls if a programme ever runs long. */
.modal-card.sheet-up { max-height: 92%; }

/* ── SEARCH: THE EMPTY LINE ────────────────────────────────────────────────
   When the match is in the other list the line is the way across, so it has
   to look like something you can press. When there is genuinely nothing, it
   is a statement and stays flat. */
.search-empty { margin: var(--sp-16) 0 0; font-size: 14px; color: var(--c-gray-500); }
.search-empty.has-jump {
  color: var(--c-black);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.search-empty.has-jump:active { color: var(--c-gray-500); }

/* ── SPECIMEN SHEET: TILES HUG ─────────────────────────────────────────────
   Pinning the meta to the bottom of a stretched row left a column of dead
   space under every short preview, which is most of them. Tiles hug their own
   content and the name sits directly under the component it names. */
.record-cat { align-items: start; }
.record-cat-meta { margin-top: 0; }

/* ── YOUR EVENTS ───────────────────────────────────────────────────────────
   R1: no filters and no subcategories on Your events — with six rows the plan
   names cost more than they explain. The filter row belongs to All events,
   where the catalogue is long enough to need narrowing. */
.event-groups.is-flat .event-group-head { display: none; }
.screen[data-screen="events"] .filter-row { display: none; }
.screen[data-screen="events"] .is-all-scope .filter-row,
.screen[data-screen="events"].is-all-scope .filter-row { display: flex; }

/* ── FACEPILE ──────────────────────────────────────────────────────────────
   Who else is going, on the row itself. Initials only — the app already names
   these people on the Profile, and a row is read at a glance. */
.facepile { display: inline-flex; align-items: center; margin-left: 6px; vertical-align: middle; }
/* Large enough to read an initial at a glance, which is the only thing a
   facepile has to do. */
.facepile i {
  width: 24px; height: 24px; margin-left: -7px;
  border-radius: 50%;
  background: var(--c-gray-200); color: var(--c-black);
  box-shadow: 0 0 0 2px var(--c-white);
  display: inline-grid; place-items: center;
  font-style: normal; font-size: 10px; font-weight: 700; letter-spacing: .02em;
}
.facepile i:first-child { margin-left: 0; }

/* ── HOME, R1 ──────────────────────────────────────────────────────────────
   "The homepage is the most relevant, time-sensitive information. It's a place
   for the symphony to communicate and market to their community." So the order
   is: what is happening, who you know going, what your pass covers this month,
   then the plans you hold, then the people who paid for the season. The
   memberships rail is still here; it is just no longer the first thing. */

/* From the symphony — a swipeable rail of what the orchestra wants you to see
   this week. Cards peek at the right edge so the rail reads as continuing. */
.promo-rail {
  display: flex; gap: var(--sp-8);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  margin-right: -18px; padding: 0 18px 2px 0;
  width: calc(100% + 18px);
}
.promo-rail::-webkit-scrollbar { display: none; }
.promo-card {
  flex: 0 0 76%;
  scroll-snap-align: start;
  display: grid; gap: 4px; align-content: start;
  padding: var(--sp-16);
  border: 0; border-radius: var(--radius-container);
  background: var(--c-white);
  text-align: left; font: inherit; cursor: pointer;
}
.promo-card:active { background: var(--c-gray-100); }
.promo-kicker {
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--c-gray-500);
}
.promo-card b {
  font-family: var(--sans); font-size: 15px; line-height: 1.2;
  font-weight: 650; color: var(--c-black);
}
.promo-card p {
  margin: 0; font-size: 12.5px; line-height: 1.35; color: var(--c-gray-500);
}

/* Friends going reuses the person row from the Profile — one shape for a
   person wherever one appears. */

/* The concerts the pass covers, directly under the count of what is left. */
.screen .held-list.claimable { margin-top: var(--sp-8); }

/* Season sponsors, as a wall at the foot of the page rather than a list in the
   middle of it. The grid takes as many logos as the season has; it currently
   has one, because one is what the content carries. */
.sponsor-wall {
  margin: var(--sp-32) 0 0;
  padding: var(--sp-24) 0 0;
  display: grid; gap: var(--sp-16); justify-items: center;
}
.sponsor-wall-head, .sponsor-wall-note {
  margin: 0; text-align: center;
  font-family: var(--sans); color: var(--c-gray-500);
}
.sponsor-wall-head {
  font-size: 10px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
}
.sponsor-wall-note { font-size: 11.5px; line-height: 1.4; max-width: 30ch; }
/* A wall, not a line: a grid of equal plates that takes as many marks as the
   season has. Each cell holds an <img> when the client has supplied the mark
   and the sponsor's name set as a wordmark when they have not — which is the
   honest placeholder, because a drawn logo would be a logo nobody owns. */
.sponsor-logos {
  list-style: none; margin: var(--sp-16) 0 0; padding: 0;
  display: grid; gap: 4px;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sponsor-logos li {
  display: grid; place-items: center;
  min-height: 76px;
  padding: var(--sp-16) 12px;
  background: var(--c-white);
  border-radius: var(--radius-inside);
  text-align: center;
}
.sponsor-logos img { max-width: 100%; max-height: 34px; width: auto; }
.sponsor-logos span {
  font-family: var(--sans); font-size: 12.5px; line-height: 1.25;
  font-weight: 650; color: var(--c-black);
  text-wrap: balance;
}

/* The bar carries a live dot only when something is live. */
.program-bar[data-state="tonight"] .live-dot { display: none; }

/* ── MEMBERSHIP, R1 ────────────────────────────────────────────────────────
   "Make the membership card more visual. I don't think we need quite so many
   details on the stats of the pass." The three-row attribute list became one
   card whose subject is the number that matters — how many claims are left —
   with the reset date as a footnote rather than a row of its own. Renewal
   terms are on the header eyebrow, where they were already stated. */
.pass-card {
  display: grid; gap: var(--sp-8);
  padding: var(--sp-24) var(--sp-16);
  border-radius: var(--radius-container);
  background: var(--c-black);
  color: var(--c-white);
}
.pass-card-kicker {
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
  color: rgb(255 255 255 / .62);
}
.pass-card-count { display: flex; align-items: baseline; gap: var(--sp-8); font-weight: 400; }
.pass-card-count b {
  font-family: var(--sans); font-size: 52px; line-height: .9; font-weight: 650;
  letter-spacing: -.02em; color: var(--c-white);
  font-variant-numeric: tabular-nums;
}
.pass-card-count em {
  font-style: normal; font-family: var(--sans);
  font-size: 13px; line-height: 1.25; color: rgb(255 255 255 / .78);
  max-width: 14ch;
}
.pass-card-track {
  display: block; height: 3px; border-radius: var(--radius-round);
  background: rgb(255 255 255 / .24); overflow: hidden;
}
.pass-card-track i { display: block; height: 100%; width: var(--pct, 50%); background: var(--c-white); }
.pass-card-foot {
  font-family: var(--sans); font-size: 11.5px; color: rgb(255 255 255 / .62);
}
.pass-card-pitch {
  margin: 0; font-size: 13.5px; line-height: 1.45; color: rgb(255 255 255 / .82);
}
/* The card is a dark surface, so the button inverts here like every other. */
.pass-card { --btn-fill: var(--c-white); --btn-ink: var(--c-black); --btn-fill-hover: var(--c-gray-200); }
.pass-card .btn { margin-top: var(--sp-8); justify-self: start; }

/* The prototype state switch says out loud that it is a prototype control, so
   it is never mistaken for something a patron would see. */
.proto-switch {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-8);
  margin-top: var(--sp-16);
  padding: 8px 10px 8px 12px;
  border-radius: var(--radius-inside);
  background: var(--c-gray-100);
}
.proto-switch-label {
  font-family: var(--sans); font-size: 9.5px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
.proto-switch-set { display: flex; gap: 4px; }
.proto-switch-set button {
  border: 0; border-radius: var(--radius-round);
  padding: 5px 10px;
  background: none; color: var(--c-gray-500);
  font: inherit; font-family: var(--sans); font-size: 11px; font-weight: 600;
  cursor: pointer;
}
.proto-switch-set button.is-active { background: var(--c-black); color: var(--c-white); }

/* ── SEARCH PUSHES OUT, RESULTS SIT ON A SHEET ─────────────────────────────
   The field used to appear underneath the header, which read as a new row of
   chrome. It now grows out of the button that opens it: the title collapses,
   the field takes the row, and a Cancel puts it back. Closed, it holds no
   width and takes no tab stop, so it is invisible to a keyboard as well.

   While searching, the results ride up on their own surface — the iOS sheet
   reading — so it is obvious you are looking at a filtered set and not the
   page you were on. */
.screen[data-screen="events"] .app-header { position: relative; overflow: hidden; }
.screen[data-screen="events"] .app-header > div:first-child {
  transition: opacity 180ms var(--ease, ease), transform 220ms var(--ease, ease);
}
.screen[data-screen="events"].is-searching .app-header > div:first-child {
  opacity: 0; transform: translateX(-12px); pointer-events: none;
}

.search-field {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 44px; margin: 0; padding: 0;
  opacity: 0; pointer-events: none; overflow: hidden;
  background: var(--c-gray-100); border: 0;
  border-radius: var(--radius-round);
  transition: width 260ms var(--ease, cubic-bezier(.2,.8,.2,1)),
              opacity 140ms ease,
              border-radius 260ms var(--ease, ease);
}
.search-field.is-open {
  width: 100%; opacity: 1; pointer-events: auto;
  padding: 0 4px 0 12px;
  border-radius: var(--radius-input);
}
.search-field .icon { flex: none; }
.search-field input { min-width: 0; }
.search-cancel {
  flex: none; border: 0; background: none; padding: 0 8px;
  color: var(--c-black); font: inherit; font-family: var(--sans);
  font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap;
}
.search-cancel:active { color: var(--c-gray-500); }

/* The sheet. It carries the scope switch and the results, so everything that
   answers the query is on one surface. */
.screen[data-screen="events"] .segments,
.screen[data-screen="events"] .event-groups,
.screen[data-screen="events"] .search-empty {
  transition: transform 260ms var(--ease, cubic-bezier(.2,.8,.2,1));
}
.screen[data-screen="events"].is-searching .segments,
.screen[data-screen="events"].is-searching .event-groups,
.screen[data-screen="events"].is-searching .search-empty {
  transform: translateY(-6px);
}
.screen[data-screen="events"].is-searching .search-sheet-ground {
  position: absolute; inset: 0; background: var(--c-white);
}
/* The button the field grew out of should not still be sitting behind it. */
.screen[data-screen="events"] [data-search-toggle] {
  transition: opacity 140ms ease, transform 200ms var(--ease, ease);
}
.screen[data-screen="events"].is-searching [data-search-toggle] {
  opacity: 0; transform: scale(.8); pointer-events: none;
}
/* The field's own ground, after the card-surface rule further up the sheet. */
.search-field, .screen .search-field {
  background: var(--c-gray-100);
  box-shadow: none;
  border: 0;
}
.search-field.is-open { padding: 0 4px 0 12px; min-height: 44px; }
.search-field input { font-size: 15px; }

/* The sheet: results ride on a white surface and the rows invert onto it, so a
   filtered set never looks like the page you were already on. */
.screen[data-screen="events"].is-searching .event-groups {
  background: var(--canvas);
  border-radius: var(--radius-container) var(--radius-container) 0 0;
  margin: var(--sp-8) -18px 0;
  padding: var(--sp-16) 18px calc(var(--safe-bottom) + 96px);
  min-height: 58vh;
}
/* The sheet takes the screen's own ground so a row looks the same inside
   search as outside it. Inverting the rows onto the sheet made the list read
   as a different component the moment the field opened, which is exactly when
   a person is scanning it hardest. */
.screen[data-screen="events"].is-searching .search-sheet-ground { background: var(--canvas); }
/* The now playing bar floats above the tab bar, so the last thing on a screen
   has to clear both. The wall is the last thing on Home. */
.sponsor-wall { padding-bottom: 76px; }

/* Rail cards match the tallest in the row, which left the shorter one with a
   block of empty card under its numbers. Each card is as tall as it needs. */
.member-rail { align-items: start; }

/* ── THE PLAN RAIL ─────────────────────────────────────────────────────────
   Three faults in one component. A mask faded the second card to a quarter
   opacity, so the card next in line read as disabled rather than as the next
   card. align-items:start let each card size to its own copy, so a two-line
   plan and a four-line plan made two different cards of the same thing. And
   82% put the peek at a seventh of the screen, wide enough to look like a
   third column that had been cut off.

   One card size, full opacity, and a peek narrow enough to read as "there is
   more" rather than as a broken layout. */
.member-rail {
  align-items: stretch;
  mask-image: none; -webkit-mask-image: none;
  padding-right: var(--sp-16);
}
.member-card { flex: 0 0 calc(100% - 44px); }
.member-rail > * { scroll-snap-align: start; }
/* The due pill was the last of a lemon accent that is not in the brand ramp —
   the same stray green that was tinting the event date. A state is carried by
   weight here, not by a hue nobody chose. */
.member-state.is-due { background: var(--c-gray-100); color: var(--c-black); border: 0; }
/* Same lemon, on the subscription hero's date line. That hero sits on black,
   where white already separates the line; it does not need a colour that is
   not in the ramp. */
.pass-billing-when { color: var(--c-white); }
/* Inviting someone and connecting a member are the same act, so they are the
   same control. On Home it is the thing the section is asking for, so it is
   drawn as one: filled, full width, under the rail. A text link at the foot of
   a carousel is not an ask. */
/* Inviting someone is the ask this section carries, so it sits in the
   heading's right slot where the count was — a tertiary action, because the
   rail below it is the subject and a filled button there would outweigh the
   people it is about. */
.friends-block .connect-btn.is-inline {
  margin: 0;
  min-height: 0;
  padding: 0;
  gap: 6px;
  background: none;
  color: var(--c-black);
  font-size: 11px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
}
.friends-block .connect-btn.is-inline .icon { width: 13px; height: 13px; }
.friends-block .connect-btn.is-inline:active { color: var(--c-gray-500); }

/* ── FRIENDS ATTENDING ─────────────────────────────────────────────────────
   The entries are concert rows cloned from the events list, so they are the
   same component and stack the same way. Nothing here styles them; they
   inherit .held-list. */
/* The list carries .held-list as well, so the rows are laid out by the same
   rules as the events list rather than by a copy of them here. */
.screen .going-list { margin-top: var(--sp-8); }

/* ── FRIENDS ──────────────────────────────────────────────────────────────
   A person is a card in a rail: the face large enough to recognise, the name
   under it, and what connects you beneath that. As rows they were two lines of
   text with a 36px plate, which told you their names and nothing else. */
.screen .friend-list {
  display: flex; gap: var(--sp-8);
  margin: var(--sp-8) calc(-1 * var(--sp-16)) 0 0;
  padding: 0 var(--sp-16) 0 0;
  overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none;
}
.screen .friend-list::-webkit-scrollbar { display: none; }
.screen .friend-list li {
  flex: 0 0 148px;
  scroll-snap-align: start;
  display: grid; justify-items: center; gap: 2px;
  padding: var(--sp-24) 12px;
  text-align: center;
}
.screen .friend-list .friend-ini {
  width: 56px; height: 56px; margin-bottom: 8px;
  border-radius: 50%;
  font-size: 15px;
}
.screen .friend-list .friend-copy { display: grid; gap: 2px; justify-items: center; }

/* ── PROFILE HIERARCHY ─────────────────────────────────────────────────────
   Measured before this: section heading 20px/650, item title 18px/650, item
   date 16px/600 near-black. Two pixels and the same weight between a heading
   and the thing under it, so nothing outranked anything and three sections
   stacked read as one undifferentiated column.

   Three steps, and only three: the heading, the thing, what the thing is. The
   count beside a heading is an annotation, not its peer. */
.screen .section-heading h2 { font-size: var(--t-subheading-md); font-weight: 650; color: var(--c-black); }
.screen .section-heading span,
.screen .section-heading button {
  font-size: 11px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-gray-500);
}
.screen .history-list b,
.screen .friend-list b,
.screen .held-list b {
  font-size: 15px; line-height: 1.25; font-weight: 650; color: var(--c-black);
}
.screen .history-list em,
.screen .history-list small,
.screen .friend-list small,
.screen .held-list em,
.screen .held-list small {
  font-size: 13px; line-height: 1.3; font-weight: 400; color: var(--c-gray-500);
}
/* The date does not earn ink after all — see the block below. */
.screen .incl-list span { font-size: 15px; font-weight: 650; }

/* ── ONE BOLD THING ────────────────────────────────────────────────────────
   Audited against ElevenLabs iOS, which is the reference for this kind of
   stacked list. What it does, and what this did not:

   · One large bold item per screen — the screen title. Nothing else competes.
   · Group labels are small, light and grey. They name the group; they do not
     announce it. Ours were 20px/650 in near-black, the same treatment as a
     screen title, three times down one page.
   · Rows carry one weight. A row's value sits in grey at the same size as its
     label, not in a second bold.
   · Two inks in the body: near-black for the thing, grey for what it is.

   Ours had four bold near-black elements stacked — heading, title, date, and
   the next heading — so the eye had no order to follow. The screen title is
   the only large bold left; a section label is a label; a row title is the
   heaviest thing in the body and the only bold; everything describing it is
   grey at one size. */
.screen .section-heading {
  margin-bottom: var(--sp-8);
  align-items: center;
}
.screen .section-heading h2 {
  font-family: var(--sans);
  font-size: 12px; line-height: 1.2; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-gray-500);
}
.screen .section-heading span,
.screen .section-heading button {
  font-size: 12px; font-weight: 400;
  letter-spacing: 0; text-transform: none;
  color: var(--c-gray-500);
}
/* One weight in the body, and it belongs to the thing itself. */
.screen .history-list em,
.screen .held-list em,
.screen .history-list small,
.screen .held-list small,
.screen .friend-list small,
.screen .notice-item p,
.screen .going-why {
  font-weight: 400; color: var(--c-gray-500);
}

/* ── THE COMPONENTS PAGE ───────────────────────────────────────────────────
   Audited against ElevenLabs iOS. Three faults, all of them the page failing
   to follow the system it documents:

   · A hover column, on a touch app that no longer has hover states. A matrix
     that documents a state which does not exist is worse than a shorter one.
     Four columns now: default, active, focus, disabled.
   · "Open member pass" was still burgundy, the only button on the page that
     had not moved to the black primary. A specimen that contradicts the
     component is a bug in the documentation, not a variant.
   · Every cell stretched its control to the column, so a small button and a
     large one drew the same width and the scale row said nothing. Controls
     sit at their own size, left-aligned on a common baseline. */
/* Columns size to the specimen, not the other way round: a button carrying a
   real label is 182px wide and a quarter of the page is 123px, so equal
   columns cut the disabled state off the right edge. The matrix scrolls if the
   labels ever outgrow the page; distorting the specimen to fit would document
   a button nobody ships. */
.ctl-matrix { overflow-x: auto; max-width: 100%; min-width: 0; }
.identity-section > div, .comp-system > div { min-width: 0; }
.ctl-row { grid-template-columns: 88px repeat(4, max-content); gap: var(--sp-8); }
.ctl-cell > :first-child { align-self: flex-start; }
/* A specimen is the control at its own size. Stretching it to the column made
   the small, medium and large buttons draw identically, which is the one thing
   the scale row exists to show. */
.ctl-cell .btn, .ctl-cell .primary-action, .ctl-cell .secondary-action { width: auto; }
.ctl-cell .field, .ctl-cell .field input { width: 100%; }
.primary-action { background: var(--c-black); color: var(--c-white); border: 0; }
.primary-action:active { background: var(--c-gray-500); }

/* ── THE CATALOGUE IS A GRID ───────────────────────────────────────────────
   Tiles hugging their own content made every row ragged: twelve cards, no two
   edges lining up. Tiles stretch to their row so each row of the grid reads as
   a row, while the rows themselves stay the height of what is in them — a
   single track height for all twelve would leave the short components sitting
   in a column of empty card.

   The preview shows the whole component. Nothing is cropped: measured, the
   tallest is the member pass at 199px scaled, and the cap that used to trim at
   208 never fires. */
.record-cat { align-items: stretch; }
.record-cat-shot { display: grid; align-content: start; }

/* ── WEB MODULES ───────────────────────────────────────────────────────────
   The marketing site's seven modules, drawn with the app's tokens at the width
   they take on a phone. Specimens, not screenshots: a picture of a module goes
   stale the first time the type scale moves, and these are built from the same
   variables as everything else, so they cannot. */
.mod-list { list-style: none; margin: var(--sp-24) 0 0; padding: 0; display: grid; gap: var(--sp-32); }
.mod-label {
  margin: 0 0 var(--sp-8);
  font-family: var(--sans); font-size: 12px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--c-gray-500);
}
.mod-note {
  margin: var(--sp-8) 0 0; max-width: 52ch;
  font-size: 13px; line-height: 1.5; color: var(--c-gray-500);
}
/* Every module is shown at 393px, the width the mock is drawn at. A module
   that only holds together at desktop width is not the module on the page. */
.mod-frame {
  width: 393px; max-width: 100%;
  border-radius: var(--radius-inside);
  overflow: hidden;
  background: var(--c-white);
}
.wm { font-family: var(--sans); color: var(--c-black); }

.wm-statement { padding: var(--sp-32) var(--sp-24) var(--sp-40); }
.wm-statement h3 {
  margin: 0; max-width: 12ch;
  font-size: 34px; line-height: 1.04; font-weight: 650; letter-spacing: -.02em;
}
.wm-statement p { margin: var(--sp-24) 0 0; font-size: 15px; line-height: 1.5; color: var(--c-black); }
.wm-statement .btn { margin-top: var(--sp-24); }

/* The bars are the measurement, so they share one baseline and one scale. */
.wm-data { display: grid; grid-template-columns: repeat(3, 1fr); min-height: 260px; }
.wm-data-col {
  display: grid; grid-template-rows: auto auto 1fr;
  align-items: start;
  padding: var(--sp-16) var(--sp-16) 0;
  border-left: 1px solid var(--c-gray-200);
}
.wm-data-col:first-child { border-left: 0; }
.wm-data-col span { font-size: 12px; color: var(--c-gray-500); }
.wm-data-col b { font-size: 22px; font-weight: 650; margin-top: 4px; }
.wm-data-col i { display: block; align-self: end; margin: var(--sp-16) -16px 0; }

.wm-bleed { position: relative; margin: 0; background: var(--c-black); }
.wm-bleed img { display: block; width: 100%; height: 320px; object-fit: cover; opacity: .72; }
.wm-bleed figcaption {
  position: absolute; inset: var(--sp-24);
  display: flex; flex-direction: column; justify-content: space-between;
  color: var(--c-white);
}
.wm-bleed b { font-size: 24px; line-height: 1.08; font-weight: 650; max-width: 11ch; }
/* The mock sets both lines from the left; the sentence reads as one sentence. */
.wm-bleed b:last-child { max-width: 14ch; }

.wm-rows { margin: 0; }
.wm-rows > div { padding: var(--sp-24); border-top: 1px solid var(--c-gray-200); }
.wm-rows > div:first-child { border-top: 0; }
.wm-rows dt { font-size: 20px; font-weight: 650; }
.wm-rows dd { margin: 6px 0 0; font-size: 14px; line-height: 1.45; color: var(--c-gray-500); }

.wm-compare { display: grid; gap: var(--sp-8); padding: var(--sp-16); background: var(--c-gray-100); }
.wm-compare section { padding: var(--sp-24); background: var(--c-white); border-radius: var(--radius-inside); }
.wm-compare h4 { margin: 0 0 var(--sp-16); font-size: 20px; font-weight: 650; }
.wm-compare h4 .lockup { height: 26px; }
.wm-compare ul { list-style: none; margin: 0; padding: 0; }
.wm-compare li {
  display: grid; grid-template-columns: 16px 1fr; gap: 12px; align-items: start;
  padding: 12px 0; border-top: 1px solid var(--c-gray-200);
  font-size: 14px; line-height: 1.4;
}
.wm-compare li:first-child { border-top: 0; }
/* One mark per side, drawn rather than coloured in: the list is the argument. */
.wm-compare li::before {
  content: ""; width: 14px; height: 14px; margin-top: 3px;
  border-radius: 3px; background: var(--c-gray-200);
}
.wm-compare li.is-for::before { background: var(--c-black); }

.wm-still { position: relative; margin: 0; background: var(--c-black); }
.wm-still img { display: block; width: 100%; height: 260px; object-fit: cover; opacity: .8; }
.wm-still-panel {
  position: absolute; right: var(--sp-16); top: var(--sp-24);
  display: grid; gap: 10px;
  padding: 14px 16px;
  border-radius: var(--radius-inside);
  background: rgb(6 6 7 / .86); color: var(--c-white);
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
}
.wm-still-head { font-weight: 650; }
.wm-still-panel > span:not(.wm-still-head) { color: var(--c-gray-300); }

.wm-close { padding: var(--sp-40) var(--sp-24); background: var(--c-black); display: grid; place-items: center; }
.wm-close .lockup { height: 64px; color: var(--c-white); }

/* ── ACCORDION AND CARD STACK ──────────────────────────────────────────────
   Both come out of the brand file's component library, which draws its eyebrow
   and its button in the placeholder blue the kit ships with. The structure is
   the module; the colour is not. The eyebrow keeps the blue the data-point bar
   already uses, so the site has one blue and one place it appears; the button
   is black, because the site has one filled button. */
.wm-eyebrow {
  margin: 0 0 var(--sp-8);
  font-size: 11px; font-weight: 650;
  letter-spacing: .1em; text-transform: uppercase;
  color: #2C6BD9;
}
.wm-acc { padding: var(--sp-32) var(--sp-24) var(--sp-40); }
.wm-acc h3 { margin: 0 0 var(--sp-24); font-size: 28px; line-height: 1.06; font-weight: 650; letter-spacing: -.02em; }
.wm-acc-list > details { border-top: 1px solid var(--c-gray-200); }
.wm-acc-list > details:last-child { border-bottom: 1px solid var(--c-gray-200); }
.wm-acc summary {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-16);
  padding: 18px 0; cursor: pointer; list-style: none;
  font-size: 17px; font-weight: 650; line-height: 1.25;
}
.wm-acc summary::-webkit-details-marker { display: none; }
/* One glyph, rotated: the minus is the plus with its upright arm hidden, so
   the two states cannot drift apart. */
.wm-acc summary i { position: relative; width: 18px; height: 18px; flex: none; }
.wm-acc summary i::before, .wm-acc summary i::after {
  content: ""; position: absolute; left: 0; top: 8px; width: 18px; height: 2px;
  background: var(--c-black);
}
.wm-acc summary i::after { transform: rotate(90deg); }
.wm-acc details[open] summary i::after { opacity: 0; }
.wm-acc details p { margin: 0 0 20px; font-size: 14px; line-height: 1.5; color: var(--c-gray-500); }

.wm-cards { padding: var(--sp-32) var(--sp-24) var(--sp-40); }
.wm-cards h3 { margin: 0 0 var(--sp-24); font-size: 28px; line-height: 1.06; font-weight: 650; letter-spacing: -.02em; }
.wm-cards ul { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-16); }
.wm-cards > ul > li { background: var(--c-gray-100); border-radius: var(--radius-container); overflow: hidden; }
.wm-card-media { margin: 0; }
.wm-card-media img { display: block; width: 100%; height: 152px; object-fit: cover; }
.wm-card-body { padding: var(--sp-24); }
.wm-card-body h4 { margin: 0; font-size: 17px; font-weight: 650; }
.wm-card-body p { margin: 8px 0 0; font-size: 14px; line-height: 1.5; color: var(--c-gray-500); }
.wm-card-body .btn { margin-top: var(--sp-16); }

/* The input carries its own minimum width, which pushed the disabled state off
   the right of the states matrix. It fills the column the buttons set. */
.ctl-cell .field input { min-width: 0; width: 100%; }

/* Section tools sit on the header rule, quiet, to the right of the heading. */
.identity-section > header { display: flex; align-items: baseline; gap: var(--sp-16); flex-wrap: wrap; }
.sec-tools { margin-left: auto; display: inline-flex; gap: var(--sp-16); }
.sec-tools .linkish {
  border: 0; background: none; padding: 0; cursor: pointer;
  font-family: var(--sans); font-size: 12px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-black); text-decoration: none;
}
.sec-tools .linkish:active { color: var(--c-gray-500); }
/* Seeing a concert a friend is at is an offer, not the errand the screen is
   for, so it reads as a text action. Three filled black buttons in a row was
   the card shouting louder than the person on it. */
.screen .going-card .going-act { min-height: 28px; padding: 0; justify-content: center; }

/* ── ICONS ─────────────────────────────────────────────────────────────────
   One set, drawn on a 24 grid with a 20 keyline. Butt caps and mitre joins:
   a round terminal adds half a stroke of blur at 18px and was the reason the
   old marks read soft next to the type. */
.icon { stroke-linecap: butt; stroke-linejoin: miter; }
/* The filled twin is a silhouette of the same drawing, so it fills rather than
   strokes. Anything ending -fill, wherever it is used. */
/* The nav was drawn in Material glyphs, which are fills, so the bar forced
   fill on every icon in it. The set is stroked now: the line twin strokes, the
   solid twin fills. Stated here because the bar's own rule says otherwise and
   sits earlier in the file. */
.bottom-nav button .nav-line { fill: none; stroke: currentColor; }
.bottom-nav button .nav-solid,
.nav-solid, .icon.is-filled { fill: currentColor; stroke: none; }

/* ── THE CONCERT ROW ───────────────────────────────────────────────────────
   The picture was 68px against a 356px card, small enough to read as a favicon
   beside the type rather than as the hall. It takes the height of the copy
   block, and the actions sit on their own line under both, so the buttons
   start at the card's edge instead of stepping in behind the thumbnail. */
.screen .held-list li > button {
  grid-template-columns: 96px minmax(0, 1fr);
  grid-template-areas: "thumb copy" "acts acts";
  align-items: start;
  gap: 12px;
  padding: 14px;
}
.screen .held-thumb { grid-area: thumb; }
.screen .held-copy { grid-area: copy; }
.screen .held-list .icon-chevron { grid-area: chev; }
.screen .held-list li > button > .row-actions { grid-area: acts; }
.screen .held-thumb { width: 96px; height: 96px; border-radius: var(--radius-inside); }
.screen .held-copy { align-self: center; }
.screen .held-list .icon-chevron { align-self: center; }
.screen .row-actions { margin-top: 12px; gap: var(--sp-8); }
.screen .row-btn { min-height: 32px; padding: 0 12px; font-size: 12.5px; }

/* The icon sheet: each mark at the size it is used, with its filled twin under
   it where one exists, so the pair is compared rather than described. */
.icon-sheet {
  list-style: none; margin: var(--sp-24) 0 var(--sp-16); padding: 0;
  display: grid; gap: 4px;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.icon-sheet li {
  display: grid; justify-items: center; gap: 6px;
  padding: var(--sp-24) var(--sp-16);
  background: var(--c-white); border-radius: var(--radius-inside);
}
.icon-plate { display: inline-flex; gap: var(--sp-16); align-items: center; margin-bottom: 6px; }
.icon-plate .icon { width: 24px; height: 24px; color: var(--c-black); }
.icon-sheet b { font-family: var(--sans); font-size: 13px; font-weight: 650; }
.icon-sheet code { font-size: 11px; color: var(--c-gray-500); }

/* ── LISTEN TO THE PROGRAM NOTES ───────────────────────────────────────────
   It was a bare row carrying its own margins, so it sat indented from the
   photograph above it and the headings below, on no surface, with a 30px black
   disc doing the work of a whole card. It is the same object as a benefit row
   or a classifier: a plate, a title, a line about it, on a card flush with
   everything else in the sheet. */
.modal-body .program-listen,
.sheet-up .program-listen {
  width: 100%;
  margin: var(--sp-16) 0 0;
  padding: 14px;
  display: grid;
  grid-template-columns: 40px minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  background: var(--c-white);
  border: 0;
  border-radius: var(--radius-inside);
}
.modal-body .program-listen:active { background: var(--c-gray-100); }
.modal-body .program-play {
  width: 40px; height: 40px;
  border-radius: var(--radius-inside);
  background: var(--c-gray-100);
  color: var(--c-black);
}
.modal-body .program-play .icon { width: 15px; height: 15px; }
.modal-body .program-listen-copy b { font-size: 15px; line-height: 1.25; font-weight: 650; color: var(--c-black); }
.modal-body .program-listen-copy small { font-size: 13px; line-height: 1.3; color: var(--c-gray-500); }

/* ── THE CONCERT ROW'S ACTIONS ─────────────────────────────────────────────
   The chevron said the whole row opens, which the two buttons under it
   already contradict — three targets on one card, one of them invisible. The
   row still opens on a tap; it does not need an arrow to say so.

   The actions split the card evenly. Two buttons sized to their own labels
   made "Claim with pass" wider than "View tickets" and left a ragged gap on
   the right of every card; equal halves make the pair one control. */
.screen .held-list .icon-chevron { display: none; }
.screen .held-list li > button > .row-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--sp-8);
}
.screen .held-list .row-btn { width: 100%; padding: 0 8px; text-align: center; justify-content: center; }

/* The segmented control was the last stroked object in the app. The track is
   the ground it sits on and the thumb is a white card, which is how every
   other pair of states is drawn here. */
.screen .segments { border: 0; background: rgb(6 6 7 / .07); }
.screen .segments button { border: 0; }
.screen .segments button.is-active { border: 0; background: var(--c-white); }

/* ── THE MEMBER PASS ───────────────────────────────────────────────────────
   Package and seats each take a mark, so the pair reads as two facts rather
   than one run of small type.

   The admission code was a stroked box holding a sentence explaining that it
   is empty — a placeholder drawn as a component. It is the pass's own surface
   with the note set as a note, because what it says is true and worth saying:
   nothing is being scanned here. */
/* Three columns, not two: the chevron is the row's third child and was
   wrapping to a line of its own under Package. */
/* Venue, tickets, seats: the three facts a pass has to answer at the door,
   each with its mark. Stacked rather than sat side by side, because three
   values across 300px is where "Orchestra A, row F" starts breaking in half,
   and because a stack gives one reading order instead of three columns to
   scan. The chevron sits against the group it opens. */
/* Three columns, not three rows: venue, tickets and seats are the same kind
   of fact and read as a set. Stacking them made a list you scan top to bottom
   for three things that answer one question. The values are set a step down so
   two words fit a third of the card without breaking a row number in half. */
.screen .pass-ticket-seats {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: start;
  column-gap: 10px;
  border: 0;
}
.screen .pass-ticket-seats > span {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "ico" "label" "value";
  justify-items: start;
  row-gap: 2px;
}
.screen .pass-ticket-seats > span > .icon { grid-area: ico; margin-bottom: 2px; }
.screen .pass-ticket-seats > .icon-chevron { display: none; }
.pass-ticket-seats > span {
  display: grid;
  grid-template-columns: 16px minmax(0, 1fr);
  grid-template-areas: "ico label" ". value";
  align-items: center;
  column-gap: 8px; row-gap: 2px;
}
.pass-ticket-seats > span > .icon { grid-area: ico; width: 14px; height: 14px; color: var(--c-gray-500); }
.pass-ticket-seats > span > em { grid-area: label; }
.pass-ticket-seats > span > b { grid-area: value; }

/* Specificity: the zone is stroked by two earlier .pass-ticket rules and lifted
   by a third, so this one has to name the screen as well. */
.screen .pass-ticket .pass-code-zone, .pass-ticket .pass-code-zone {
  border: 0 !important;
  box-shadow: none !important;
  background: var(--c-gray-100);
  border-radius: var(--radius-inside);
  padding: var(--sp-24) var(--sp-16);
  text-align: center;
}
.pass-ticket .pass-code-zone span {
  display: block;
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
.pass-ticket .pass-code-zone small {
  display: block; margin-top: 6px;
  font-size: 12.5px; line-height: 1.4; color: var(--c-gray-500);
}

/* ── PLAN STATUS ───────────────────────────────────────────────────────────
   The card set the plan against its renewal date on one line, so a title, a
   description and a date in the accent all competed across 300px. The plan is
   the subject; when it renews is a footnote under it. */
.screen .plan-strip {
  display: grid; gap: 6px;
  padding: 14px;
  background: var(--c-white);
  border: 0; border-radius: var(--radius-inside);
}
.screen .plan-strip > span:first-child { display: grid; gap: 2px; }
.screen .plan-strip b { font-size: 15px; font-weight: 650; color: var(--c-black); }
.screen .plan-strip small { font-size: 13px; color: var(--c-gray-500); }
.screen .plan-flag {
  justify-self: start;
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-gray-500);
}

/* The notifications control is a plate like every other icon plate, not the
   one circle on the screen. The unread dot sits on its corner. */
.round-btn { border-radius: var(--radius-inside); }
.round-btn.has-unread::before { top: 6px; right: 6px; }

/* ── RENEWAL ───────────────────────────────────────────────────────────────
   The one card on the screen that is asking for something rather than
   reporting something, so it is the one card drawn on the ink. A mark, a line
   that says what the offer is, the terms under it, and the action.

   It inverts the pair of button variables rather than restyling the buttons,
   so the filled action stays the filled action and simply reads white here. */
.screen .renew-block {
  --btn-fill: var(--c-white);
  --btn-ink: var(--c-black);
  --btn-fill-hover: var(--c-gray-200);
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: grid;
  gap: 14px;
  padding: var(--sp-32) var(--sp-24);
  background: var(--c-black);
  border: 0;
  border-radius: var(--radius-container);
}
/* The house, not a pictogram of a chair. A seat icon described the offer; the
   room the seats are in makes the case for keeping them. Held back far enough
   that the type is the first thing read — it is ground, not content. */
.screen .renew-block::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: url("assets/maestro-canon-audience.webp") center 30% / cover no-repeat;
  opacity: .46;
}
.screen .renew-block::after {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgb(6 6 7 / .45) 0%, rgb(6 6 7 / .82) 68%);
}
.renew-mark {
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border-radius: var(--radius-inside);
  background: rgb(255 255 255 / .12);
  color: var(--c-white);
}
.renew-mark .icon { width: 20px; height: 20px; }
.renew-copy { display: grid; gap: 6px; }
.renew-copy > b {
  font-family: var(--sans); font-size: 17px; line-height: 1.2;
  font-weight: 650; color: var(--c-white);
}
.screen .renew-block p {
  margin: 0;
  font-size: 13.5px; line-height: 1.45;
  color: var(--c-gray-300);
}
.screen .renew-block p b { color: var(--c-white); font-weight: 650; }
.screen .renew-actions {
  justify-items: start;
  gap: 12px;
  margin-top: 4px;
}
.screen .renew-block .btn--tertiary { color: var(--c-gray-300); }
.screen .renew-block .btn--tertiary:active { color: var(--c-white); }

/* ── ONE RAIL ──────────────────────────────────────────────────────────────
   Four horizontal rails had drifted into four treatments: different gaps,
   different peeks, one with a mask that faded its second card to a quarter
   opacity and three with a hard cut at the frame edge. One rail now.

   The trailing fade is 28px of mask, enough to say the row continues and not
   enough to dim a card anyone is reading — which is what the old mask got
   wrong. It runs to the phone's edge, so the rail bleeds the screen padding
   and the fade sits on the frame rather than 18px inside it. */
/* The filter chips scroll like every other rail and were the one left with a
   hard cut at the frame edge, so "Have tickets" ended mid-word against the
   bezel. Same treatment as the rest. */
.screen .promo-rail,
.screen .member-rail,
.screen .friend-list,
.screen .badge-rail,
.screen[data-screen="events"] .filter-row {
  display: flex;
  gap: var(--sp-8);
  align-items: stretch;
  margin: var(--sp-8) -18px 0;
  padding: 0 18px;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: 18px;
  scrollbar-width: none;
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
}
.screen[data-screen="events"] .filter-row::-webkit-scrollbar,
.screen .promo-rail::-webkit-scrollbar,
.screen .member-rail::-webkit-scrollbar,
.screen .friend-list::-webkit-scrollbar,
.screen .badge-rail::-webkit-scrollbar { display: none; }

.screen .promo-rail > *,
.screen .member-rail > *,
.screen .friend-list > li,
.screen .badge-rail > li,
.screen[data-screen="events"] .filter-row > * { scroll-snap-align: start; }

/* One peek for every rail, so the amount of the next card you can see is a
   property of the rail and not of whatever is in it. */
.screen .promo-rail > *,
.screen .member-rail > * { flex: 0 0 calc(100% - 44px); }
/* The facepile is a line of the row, under the venue: initials, then who. */
.screen .held-copy > .facepile {
  margin: 6px 0 0;
  align-self: start;
}
/* The classifier rows carry a meter that already says there is more behind
   them, and the whole row is the target. The chevron was a third mark on a row
   that needs two. */
.screen .patron-classifiers .icon-chevron { display: none; }
.screen .patron-classifiers li > button { grid-template-columns: 40px minmax(0, 1fr) auto; }

/* ── THIS SEASON ───────────────────────────────────────────────────────────
   The one card on the Profile that is a record of what you have done rather
   than a control, so it is the one that gets a picture. The hall sits behind
   it, blurred and darkened, and the figures read white on top — the image is
   ground, not content, which is why it is out of focus: a sharp photograph
   here would compete with the numbers it is behind.

   Same construction as the renewal card: the surface changes, the component
   does not. */
.screen .stat-row {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--c-black);
  box-shadow: none;
  border-radius: var(--radius-container);
}
.screen .stat-row::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: url("assets/maestro-canon-brass.webp") center / cover no-repeat;
  filter: blur(14px) saturate(.7);
  transform: scale(1.15);
  opacity: .55;
}
.screen .stat-row::after {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgb(6 6 7 / .35), rgb(6 6 7 / .72));
}
.screen .stat-row > div { padding: var(--sp-24) var(--sp-16); }
.screen .stat-row dt { color: rgb(255 255 255 / .62); }
.screen .stat-row dd { color: var(--c-white); }
.screen .stat-row dd span { color: rgb(255 255 255 / .62); }

/* ── PROFILE RHYTHM ────────────────────────────────────────────────────────
   Eight sections stacked on a 24px step, with cards 4 to 8px apart inside
   each, so the gap between two badges was nearly the gap between Badges and
   Attendance history. The step between sections is 40px now; the step inside
   one stays 8. Two levels of space for two levels of structure. */
.screen[data-screen="account"] .section-block { margin-top: var(--sp-40); }
.screen[data-screen="account"] .section-heading { margin-bottom: 12px; }
.screen[data-screen="account"] .settings-list { margin-top: var(--sp-40); }
.screen[data-screen="account"] .connect-btn { margin-top: 12px; }

/* ── CLAIMS, AS A STEPPER ──────────────────────────────────────────────────
   A continuous bar at 50% describes a proportion. Four claims a month is a
   count, and the thing a person wants to know is how many are left, not what
   share is gone. Four segments, two lit — the same meter the classifier rows
   use, so a count looks like a count everywhere in the app. */
.screen .challenge-meter { margin-top: 12px; gap: 4px; }
.screen .challenge-meter i { width: 22px; height: 4px; }

/* ── METERS ARE STUBS ──────────────────────────────────────────────────────
   The same mark as the Harmony Pass card, smaller. A classifier counts things
   you hold — six concerts in the package, three benefits on the pass, two
   people in the household — and the ticket stub is how this app draws one of
   those. A bar, then a stepper, then a stub in one place and a stepper in
   another: this is the last of it.

   The stubs carry the count on their own, so the leading-edge cap that marked
   position on the stepper is gone with the stepper. */
/* Six of them plus the gaps was 111px, which pushed "Classical six-show" onto
   two lines. Tightened to fit the longest classifier on one. */
/* A row of little tickets counted concerts by drawing the object you get for
   one, which put a picture of a ticket in three places that are not about
   tickets — a subscription's attendance, a membership's benefits. Segments of
   a bar say two of six without illustrating anything, and they are the same
   bar Season progress uses on Home, one notch per unit instead of a fill. */
.classifier-meter { display: flex; align-items: center; gap: 3px; }
.classifier-meter i {
  width: 13px; height: 4px;
  border-radius: var(--radius-round);
  background: var(--c-gray-200);
}
.classifier-meter i.is-on { background: var(--c-black); }

/* ── MEMBER BENEFITS ───────────────────────────────────────────────────────
   The hierarchy was inverted: the benefit already used carried a filled black
   pill, the two you can still act on carried outlines. The strongest control
   on the screen was the one that does nothing.

   What you can do is filled. What is done is a quiet mark and a word, with no
   control drawn around it, because there is nothing left to press. The
   outlines go with it — the register has no stroked buttons. */
.screen .benefit-list button > span:last-child {
  min-height: 34px;
  padding: 0 14px;
  border: 0;
  border-radius: var(--radius-button);
  background: var(--c-black);
  color: var(--c-white);
  font-family: var(--sans); font-size: 11px; font-weight: 400;
  letter-spacing: .085em; text-transform: uppercase;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
}
.screen .benefit-list button > span:last-child.is-done {
  background: none;
  color: var(--c-gray-500);
  padding: 0;
  min-height: 0;
}
.screen .benefit-list button > span:last-child.is-done .icon {
  width: 13px; height: 13px; color: var(--c-gray-500);
}
/* The used row's note is a note like any other. */
.screen .benefit-list small.is-used { font-weight: 400; color: var(--c-gray-500); }


/* ── ANNOUNCEMENTS ─────────────────────────────────────────────────────────
   It was the one full-screen cover in an app whose every other overlay is a
   sheet — the programme, the filters, the seat, the afterglow, the benefits.
   A notice list is scanned and left, not navigated to, and keeping the screen
   behind it visible is what tells you that.

   It keeps the height it needs: at 88% it still reads as the thing you are
   dealing with, which was the point of making it full-screen in the first
   place. The difference is that it arrives from the bottom, dismisses on the
   scrim, and is the same object as everything else that opens over a screen. */
.modal-card.is-notices { max-height: 88%; }
.is-notices .modal-head { padding: var(--sp-16) var(--sp-16) var(--sp-8); }
.is-notices .modal-head h2 { font-size: var(--t-subheading-lg); }
.is-notices .modal-body { flex: 1; overflow: auto; padding-bottom: calc(var(--safe-bottom) + var(--sp-24)); }
.is-notices .modal-body .section-heading { margin: 0; padding: var(--sp-16) var(--sp-16) var(--sp-8); }
.is-notices .notice-item { padding: 14px var(--sp-16); }
.stage-over--up .menu-scrim { background: rgb(6 6 7 / .32); }

/* ── STAGE CONTROLS ────────────────────────────────────────────────────────
   The prototype-state switch is a control for whoever is reviewing the build,
   not one a patron would ever see. Inside the phone it read as a feature of
   the app — a settings row nobody asked for on the Membership screen. It sits
   beside the device now, on the page, where the view tabs are. */
.stage-controls {
  display: flex; justify-content: center;
  margin: var(--sp-24) 0 0;
}
.stage-controls .proto-switch {
  display: inline-flex; align-items: center; gap: var(--sp-16);
  padding: 8px 8px 8px 16px;
  border-radius: var(--radius-round);
  background: var(--c-white);
}
.stage-controls .proto-switch-label {
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
.stage-controls .proto-switch-set { display: inline-flex; gap: 4px; }
.stage-controls .proto-switch-set button {
  border: 0; cursor: pointer;
  min-height: 30px; padding: 0 14px;
  border-radius: var(--radius-round);
  background: none; color: var(--c-gray-500);
  font-family: var(--sans); font-size: 12px; font-weight: 600;
}
.stage-controls .proto-switch-set button.is-active { background: var(--c-black); color: var(--c-white); }

/* ── HOME: LESS HERO, MORE PAGE ────────────────────────────────────────────
   The hero was taking two thirds of the frame, so the first thing under it
   arrived at the fold and everything Home is actually for sat below. It keeps
   the photograph and the action; it gives back the height.

   Sections step on 40 like the Profile, so a gap between two sections no
   longer measures the same as a gap between two cards inside one. */
.screen[data-screen="home"] .event-hero { min-height: 0; }
.screen[data-screen="home"] .event-hero .hero-img { height: 360px; }
.screen[data-screen="home"] .hero-body { padding-bottom: var(--sp-24); }
.screen[data-screen="home"] .hero-body h1 { font-size: var(--t-heading-sm); }
.screen[data-screen="home"] .section-block { margin-top: var(--sp-40); }
.screen[data-screen="home"] .home-greet { margin-bottom: var(--sp-8); }

/* The map's step markers and path take the ink, not a colour that is no
   longer in the palette. */
.site-node[data-step] { border-color: var(--c-black); box-shadow: 0 0 0 2px rgb(6 6 7 / .12); }
.swatch-path, .swatch-accent { background: var(--c-black); }
.site-node .node-ico { border-radius: var(--radius-inside); }

/* ── THE HARMONY PASS CARD ─────────────────────────────────────────────────
   The house behind the pass, on the same terms as the renewal card: held back
   under a gradient so the count is still the first thing read. A pass is worth
   something because of the room it gets you into, and the card was a black
   rectangle saying so in words.

   Its progress bar goes with it. Home counts these claims in ticket stubs; a
   bar here described the same four admissions as a share. One count, one
   drawing of it. */
.screen .pass-card {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.screen .pass-card::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: url("assets/maestro-canon-audience.webp") center 35% / cover no-repeat;
  opacity: .5;
}
.screen .pass-card::after {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(180deg, rgb(6 6 7 / .42) 0%, rgb(6 6 7 / .84) 72%);
}
/* The offer is the same card with different strings: same ground, same kicker,
   same figure, same four stubs — all four available, because that is what the
   pass gives you. Only the words and the action change. I had given the offer
   a plain ground on the argument that selling should not lean on a photograph
   of using it; one component with two grounds is two components. */
.screen .pass-card.is-offer .btn { margin-top: var(--sp-16); justify-self: start; }
.screen .pass-card.is-offer .pass-card-foot { max-width: 30ch; }

.screen .pass-card 
/* ── THE PASS COUNT ────────────────────────────────────────────────────────
   Four things were competing: the kicker, the figure, a unit wrapping to two
   lines beside it, and the reset date. Three levels now — the figure with
   "of 4" on its baseline, what it counts on the line under, when it resets as
   a footnote. */
.screen .pass-card-count { display: flex; align-items: baseline; gap: 8px; }
.screen .pass-card-count em {
  font-style: normal;
  font-family: var(--sans); font-size: 15px; font-weight: 600;
  color: rgb(255 255 255 / .62);
}
.screen .pass-card-unit {
  display: block; margin-top: 2px;
  font-family: var(--sans); font-size: 15px; line-height: 1.25;
  color: var(--c-white);
}
.screen .pass-card-foot {
  font-family: var(--sans); font-size: 12.5px;
  color: rgb(255 255 255 / .55);
}

/* ── NOTHING TRUNCATED ─────────────────────────────────────────────────────
   The venue line was clipped with an ellipsis, which hid the one thing the
   row was there to say — "included with the pass". A concert row is tall
   enough to carry two lines, so the line wraps instead of being cut. The
   title already wraps; this makes the rest of the row behave the same. */
.screen .held-list small,
.screen .held-copy small {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

/* ── THE HERO ACTION IS SECONDARY ──────────────────────────────────────────
   Showing your pass is not the errand of the home screen; it is one of the
   things you can do from it, and a filled black button on a black photograph
   was both the heaviest object on the page and the hardest to see. A
   secondary on this ground is the white the type is already in, held back far
   enough to sit under the headline. */
.screen .hero-cta {
  background: rgb(255 255 255 / .16);
  color: var(--c-white);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}
.screen .hero-cta:active { background: rgb(255 255 255 / .28); }

/* ── THE ANNOUNCEMENTS SHEET ───────────────────────────────────────────────
   Three faults. It opened at 88% but the head reserved so much room that the
   first notice arrived a third of the way down, so the sheet looked half
   empty while its last item sat below the fold. It also sat 8px off the
   bottom of the frame, which read as a card that had not finished arriving.
   And its top corners were the container radius where every other sheet in
   the app uses it at the inside step.

   It reaches the foot of the frame, the head takes the room a head needs, and
   the corners match the programme sheet. */
/* Announcements is a tall sheet, and it rises.
   Three positions in one build, so the reasoning is worth keeping. Full-screen,
   then a sheet, then a page again on the argument about volume — a season of
   notices is a list you work through. The volume argument holds; covering the
   frame to the last pixel does not. A sheet that leaves the screen showing
   above it says what it is and where it came from, and that strip of the app
   behind it is also the thing you tap to leave.
   It keeps a floor as well as a ceiling, so three notices and thirty open to
   the same place. */
.modal-card.is-notices {
  inset: auto 0 0 0;
  min-height: 72%;
  max-height: 88%;
  border-radius: var(--radius-container) var(--radius-container) 0 0;
}
/* The scrim is the second way out. A page with no scrim left the handle as the
   only target, which is a 28px strip at the top of a full-height surface. */
.stage-over--up:has(.is-notices) .menu-scrim { display: block; background: rgb(6 6 7 / .32); }
.is-notices .modal-head { padding: var(--sp-8) var(--sp-16) 0; }
.is-notices .modal-head { padding: calc(var(--safe-top) + var(--sp-16)) var(--sp-16) 0; }
.is-notices .modal-head { padding: var(--sp-8) var(--sp-16) 0; }
.is-notices .modal-head h2 { margin: 0; }
.is-notices .modal-body { padding-top: var(--sp-8); }
.is-notices .modal-body > .section-block:first-child,
.is-notices .modal-body > .notice-list:first-child { margin-top: 0; }
.is-notices .modal-body .section-heading:first-child { padding-top: var(--sp-16); }

/* ── FROM THE SYMPHONY ─────────────────────────────────────────────────────
   These cards carry notices — a programme change, a concert new on the pass,
   a renewal date — which is the same content the announcements sheet lists.
   They are the notice component now: the same plate, the same two type steps,
   the same icon vocabulary. A notice is a notice whether it is in the
   carousel or in the sheet; two drawings of it was the thing to fix, not the
   radius, which was already 16 everywhere. */
.screen .promo-card {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  align-items: start;
  gap: 12px;
  padding: var(--sp-16);
  text-align: left;
}
.screen .promo-card .notice-ico { color: var(--c-gray-500); }
.screen .promo-card .notice-copy { display: grid; gap: 2px; min-width: 0; }
.screen .promo-card .promo-kicker {
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
.screen .promo-card b { font-size: 15px; line-height: 1.25; font-weight: 650; color: var(--c-black); }
.screen .promo-card .notice-copy > span:last-child {
  font-size: 13px; line-height: 1.4; color: var(--c-gray-500);
}
/* The pile names itself: two initials in a grey circle are a puzzle without a
   label, and the row has the width for four words. */
/* The pile is faces then names then the word that says what it is: "Jordan
   Alvarez, A. Koczon attending". Initials alone needed a label above them to
   mean anything; the names carry it themselves. */
.screen .held-copy > .facepile {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px;
}
.facepile-faces { display: inline-flex; align-items: center; }
.facepile-names {
  font-family: var(--sans); font-size: 12.5px; line-height: 1.3;
  color: var(--c-gray-500); min-width: 0;
}
.facepile-label {
  font-style: normal;
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-gray-500);
}

.facepile i:first-of-type { margin-left: 0; }

/* ── CLAIMED AND UNCLAIMED ─────────────────────────────────────────────────
   Ink against gray-200 said "two of these are different" without saying which
   way round. A spent claim is a torn stub: it keeps its width and loses its
   height, so the two you can still use stand a head taller than the two you
   cannot. The difference is now a silhouette rather than a shade, which
   survives being glanced at.

   The card also ends on the thing to do about it. A prompt that states a
   balance and offers no way to spend it is a notice, not a prompt. */
.screen .challenge { display: grid; justify-items: start; }
.challenge-go {
  margin-top: var(--sp-16);
  display: inline-flex; align-items: center;
  min-height: 34px; padding: 0 14px;
  border-radius: var(--radius-button);
  background: var(--c-black); color: var(--c-white);
  font-family: var(--sans); font-size: 11px; font-weight: 400;
  letter-spacing: .085em; text-transform: uppercase;
}
.screen .challenge:active .challenge-go { background: var(--c-gray-500); }

/* ── CLAMPED TO THE FRAME ──────────────────────────────────────────────────
   The closed search field parks its Cancel button 61px past the right edge of
   the device. An overflow:hidden ancestor was hiding it, which is not the same
   as it not being there: it still took a box outside the frame and still sat
   in the tab order. Closed, it is not rendered at all.

   Everything else was already inside. Audited every element in the phone
   across the five screens and the announcement and programme sheets, counting
   only what is not in a rail that scrolls on purpose. */
.screen[data-screen="events"]:not(.is-searching) .search-cancel {
  display: none;
}

/* ── THE PASS'S CHEVRON ────────────────────────────────────────────────────
   It was centred against the whole two-line pair, which put it between the
   label and the value rather than on either. It sits on the value, which is
   the line it opens, and tight to the card's own padding instead of floating
   in the gutter. */
.screen .pass-ticket-seats { align-items: end; }
.screen .pass-ticket-seats > .icon-chevron {
  align-self: end;
  margin-bottom: 1px;
  color: var(--c-gray-300);
}

.screen[data-screen="events"] .filter-row > * { flex: 0 0 auto; }
.screen[data-screen="events"].is-all-scope .filter-row { display: flex; }
/* The tab bar sits on its own ground; the rule above it was a second edge
   doing the same job as the change of surface. */
.bottom-nav { border-top: 0; box-shadow: none; }

/* ── A METER COUNTS IN THE MARK OF THE THING ───────────────────────────────
   Six tickets for six concerts and three for three benefits are admissions,
   which is what a stub is. Two tickets for two people were not: a household
   is people, and drawing them as admissions was consistency applied past the
   point where it meant anything. The household row counts in faces — the same
   initials mark the facepile and the friend cards already use. One rule: the
   meter counts in the mark of the thing it is counting. */
.classifier-meter.is-people { gap: 0; }
.classifier-meter.is-people b {
  width: 20px; height: 20px; margin-left: -4px;
  border-radius: 50%;
  background: var(--c-gray-200); color: var(--c-black);
  box-shadow: 0 0 0 2px var(--c-white);
  display: inline-grid; place-items: center;
  font-size: 8px; font-weight: 700; letter-spacing: .02em;
}
.classifier-meter.is-people b:first-child { margin-left: 0; }
/* The five destinations carry the filled twin, the same weight the tab bar
   shows when you are standing on them. The plate they sit in is already ink,
   so the mark fills white rather than stroking. */
.site-node .node-ico svg[data-filled] { fill: currentColor; stroke: none; }

/* ── HOME'S OPENING LINES ──────────────────────────────────────────────────
   The greeting was the largest thing on the screen and the least useful, set
   at heading size in grey — which takes the most space and still reads as
   inactive, the worst of both. It is a line of address, so it is set as one:
   the date above it, the name in ink, at a size that lets the first card be
   the loudest thing on the page. */
.screen[data-screen="home"] .home-greet {
  font-family: var(--sans);
  font-size: var(--t-subheading-md); line-height: 1.15;
  font-weight: 650; letter-spacing: -.01em;
  color: var(--c-black);
  margin: 2px 0 0;
}
.screen[data-screen="home"] .home-date {
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--c-gray-500);
}
.screen[data-screen="home"] .section-block:first-of-type { margin-top: var(--sp-32); }
/* The rail block gives the filter row display:flex, which was overriding the
   rule that hides it on Your events. Stated last, and scoped, so the scope
   decides rather than the order of two unrelated blocks. */
.screen[data-screen="events"]:not(.is-all-scope) .filter-row { display: none; }

/* ── THE SPECIMEN PLATE ────────────────────────────────────────────────────
   The plate was sized by whatever it held, so twelve components produced
   twelve plates: a sponsor cell made a short one, a member pass made a tall
   one, and a rail card sat hard against the left of a plate twice its width.
   The component was standardised long ago; the thing it sits on was not.

   One plate. It takes the height its row gives it, so every plate in a row
   matches, and the component is centred in it rather than parked in a corner.
   The name still sits directly under the plate, which is what stops the row
   from leaving a column of dead card under the short ones. */
/* place-content centres the track; the stage still stretched inside it, which
   left a short component sitting at the top of a 200px plate. place-items
   centres the item as well. */
.record-cat-shot {
  flex: none;
  height: 200px;
  display: grid;
  place-content: center;
  place-items: center;
}
.record-cat-shot .cmp-stage { margin: 0 auto; }

/* ── THE TAB BAR'S EDGE ────────────────────────────────────────────────────
   Content met the bar on a hard line: a row of buttons cut mid-word and then
   a white bar, which reads as a crop rather than as something passing behind.
   A band above the bar blurs and fades what is scrolling under it, so the list
   goes out of focus on its way beneath rather than stopping at an edge.

   Drawn on the bar rather than on the screen because the bar is the thing that
   floats; a screen that scrolls cannot hold a fixed band. */
/* The bar keeps the absolute position that pins it to the foot of the frame —
   setting it to relative put it back in flow and dropped it off the bottom of
   the screen. An absolutely positioned element hosts a ::before perfectly
   well; it did not need repositioning to carry one. */
.bottom-nav::before {
  content: "";
  position: absolute; left: 0; right: 0; bottom: 100%;
  height: 32px;
  pointer-events: none;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  background: linear-gradient(to top, var(--canvas), rgb(246 245 243 / 0));
  -webkit-mask-image: linear-gradient(to top, #000, transparent);
  mask-image: linear-gradient(to top, #000, transparent);
}

/* ── THE SEASON LIST ───────────────────────────────────────────────────────
   Your season was drawn as its own component — journey-entry, with its own
   thumbnail, copy and people classes — which made a second concert row that
   had to be restyled every time the first one changed. It is the concert row
   now, in the states the season needs: attended rows drop back, tonight's row
   does not. Six line items, one component.

   The season list is a record of a package rather than a list to act on, so
   it carries no row actions; the events list is where a concert is claimed. */
.screen .season-list .held-thumb { width: 72px; height: 72px; }
.screen .season-list li > button { grid-template-columns: 72px minmax(0, 1fr); padding: 12px 14px; }
.screen .season-list li.is-done { opacity: .5; }
.screen .season-list li.is-done .held-thumb { opacity: 1; }

/* Nothing on the pass is drawn with a line. The card, the detail group and the
   admission plate all separate by surface, which is how every other card in
   the app does it. */
.screen .pass-ticket,
.screen .pass-ticket *,
.screen .pass-ticket-seats,
.screen .pass-code-zone { border: 0; box-shadow: none; }
.screen .pass-ticket .btn--primary { box-shadow: none; }
.screen .pass-ticket-seats > span > em {
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
.screen .pass-ticket-seats > span > b {
  font-family: var(--sans); font-size: 13.5px; line-height: 1.25;
  font-weight: 650; color: var(--c-black);
}
.screen .pass-ticket-seats > span > em { font-size: 9.5px; letter-spacing: .07em; }

/* The season rows carry what the package covers, so each says so rather than
   a heading over a second copy of the same six concerts. */
.season-tag {
  display: inline-block; margin-top: 6px;
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--c-gray-500);
}
/* Book carries one action: add to what you hold. It sits where the search
   control sits on Events, so the two screens put their action in the same
   place. */
.screen[data-screen="subscription"] .app-header { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-16); }
.book-add { flex: none; }

/* ── THE SEASON CALENDAR ───────────────────────────────────────────────────
   A month at a time, on the months the season actually spans. The grid is the
   shape of a month, so the marks land where a person already expects to find
   a date; a list cannot do that. */
.season-cal .cal {
  background: var(--c-white);
  border-radius: var(--radius-container);
  padding: var(--sp-16);
}
.cal-head {
  display: grid; grid-template-columns: 32px minmax(0, 1fr) 32px;
  align-items: center; margin-bottom: 12px;
}
.cal-head b {
  text-align: center;
  font-family: var(--sans); font-size: 15px; font-weight: 650; color: var(--c-black);
}
.cal-step {
  width: 32px; height: 32px; border: 0; background: none; cursor: pointer;
  display: grid; place-items: center; color: var(--c-black);
  border-radius: var(--radius-inside);
}
.cal-step:disabled { color: var(--c-gray-200); cursor: default; }
.cal-step:active:not(:disabled) { background: var(--c-gray-100); }
.cal-step .icon { width: 16px; height: 16px; }

.cal-week, .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); }
.cal-week { margin-bottom: 4px; }
.cal-week i {
  font-style: normal; text-align: center;
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .06em; color: var(--c-gray-300);
}
.cal-grid { gap: 2px; }

/* A day is a control only where there is something on it. */






/* Picking a day walks you to the row rather than filtering the list, so the
   season stays whole and the calendar is a way in rather than a filter. */
.season-list li.is-picked > button { background: var(--c-gray-100); }

/* ── THE PASS'S THREE FACTS, ON ONE LINE EACH ──────────────────────────────
   The columns were grids inheriting areas from the two-column version they
   replaced, so each one placed its mark wherever its own content allowed and
   the three icons sat at three heights. A column is a stack: mark, label,
   value, in that order, from a shared top edge. The mark's row is a fixed
   height so the labels line up whether a value wraps or not. */
.screen .pass-ticket-seats > span {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.screen .pass-ticket-seats > span > .icon {
  height: 16px; width: 16px;
  margin: 0 0 4px;
  flex: none;
}
.screen .pass-ticket-seats > span > em { order: 2; }
.screen .pass-ticket-seats > span > b { order: 3; }
/* The middle column holds one line where its neighbours hold two, so a centred
   row dropped its mark half a line below theirs. The three columns hang from a
   shared top edge. */
.screen .pass-ticket-seats { align-items: start !important; }

/* The chip row bled to the bezel, so the last chip was cut by the device
   rather than fading inside it. It keeps the bleed — a rail that stops at the
   content margin reads as a list that has ended — but the fade is wide enough
   to do its work and the row carries trailing padding, so nothing is ever
   sliced against the edge of the frame. */
.screen[data-screen="events"] .filter-row {
  padding-right: 34px;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 52px), transparent 100%);
  mask-image: linear-gradient(to right, #000 calc(100% - 52px), transparent 100%);
}

/* ── BADGES ON THE PORTRAIT ────────────────────────────────────────────────
   Three of them, sitting on the lower edge of the portrait so they read as
   something the person carries rather than a list about them. Cloned from the
   badge rail, which stays as the full set with the names attached — a mark on
   a portrait can show what you have earned, but it cannot say what it is. */
.patron-profile { position: relative; }
/* Badges belong to the person, so they sit with the name rather than pinned
   over the picture — an overlay reads as a sticker on a photograph, and the
   ring had to be measured against the portrait every time it resized. Here
   they are part of the identity block, on the line under the name. */
.patron-badges {
  display: inline-flex;
  margin-top: 8px;
}
.patron-badges i {
  width: 24px; height: 24px;
  margin-left: -6px;
  display: inline-grid; place-items: center;
  border-radius: 50%;
  background: var(--c-black);
  color: var(--c-white);
  box-shadow: 0 0 0 2px var(--canvas);
}
.patron-badges i:first-child { margin-left: 0; }
.patron-badges .icon { width: 12px; height: 12px; }
/* The season's own scope switch, on the same control as Search's. */
.screen .season-scope { margin: var(--sp-8) 0 var(--sp-16); }
/* A past concert is a record, not a thing to act on, so it keeps the faded
   reading it has in the list rather than being hidden behind it. */
.screen .season-list li.is-done { opacity: .62; }

/* ── BROWSE BY CATEGORY ────────────────────────────────────────────────────
   The CMS's whole category vocabulary, three across. A category with no
   concert this season stays in the grid and says so: hiding it would make the
   list look complete when it is not, and a CMS's vocabulary is a fact about
   the system rather than about this month's programming. */
.cat-grid {
  list-style: none; margin: var(--sp-8) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px;
}
.cat-grid button {
  width: 100%;
  display: grid; justify-items: center; gap: 2px;
  padding: var(--sp-16) 8px;
  border: 0; border-radius: var(--radius-inside);
  background: var(--c-white);
  text-align: center; font: inherit; cursor: pointer;
}
.cat-grid button:active { background: var(--c-gray-100); }
.cat-ico {
  width: 40px; height: 40px; margin-bottom: 6px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--c-gray-100); color: var(--c-black);
}
.cat-ico .icon { width: 18px; height: 18px; }
.cat-grid b {
  font-family: var(--sans); font-size: 13px; line-height: 1.2;
  font-weight: 650; color: var(--c-black);
}
.cat-grid small {
  font-family: var(--sans); font-size: 11px; line-height: 1.25;
  color: var(--c-gray-500);
}
/* Nothing to browse is not a control. */
.cat-grid button.is-empty { cursor: default; }
.cat-grid button.is-empty .cat-ico { color: var(--c-gray-300); }
.cat-grid button.is-empty b { color: var(--c-gray-500); }
.cat-block .cat-all {
  border: 0; background: none; padding: 0; cursor: pointer;
  font-family: var(--sans); font-size: 12px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--c-black);
  text-decoration: none;
}

/* ── THE ANNOUNCEMENTS SHEET, TIDIED ───────────────────────────────────────
   The carets go: a notice row is the whole target and the sheet has one way
   out, so a chevron on each row was a third mark competing with the icon and
   the title for the same glance.

   The handle sits in its own band above the title rather than overlapping it,
   with the bar centred in a target a thumb can find. */
.is-notices .notice-item .icon-chevron { display: none; }
.is-notices .notice-item { grid-template-columns: 28px minmax(0, 1fr); }
.is-notices .sheet-grip { height: 32px; }
.is-notices .sheet-grip::before { top: 12px; width: 44px; height: 5px; }
.is-notices .modal-head { padding: 4px var(--sp-16) 0; }
.is-notices .modal-head h2 { font-size: var(--t-subheading-lg); }

/* ── THE CALENDAR IN FULL ──────────────────────────────────────────────────
   A day that holds a concert carries the concert's picture, perforated top
   and bottom — the same cut the ticket stubs use, so a date you hold looks
   like something you hold. The number sits on the picture rather than beside
   it, because the picture is the cell. */




/* A stamp needs room to be a picture. */
.cal-grid { gap: 4px; }
/* The calendar's own switch, on the same control the season and the scopes
   use. It sits above the month so the reading is chosen before it is read. */
.cal-mode { margin-bottom: 12px; }


/* ── THE HANDLE IS A BUTTON NOW ────────────────────────────────────────────
   The original .sheet-grip was a 36x4 bar with its own fill and radius. Making
   it a <button> kept those and added the user agent's border and padding, so
   it drew as a grey blob above the title. The button is a bare target; the bar
   is drawn inside it. */
.modal-card .sheet-grip,
.is-notices .sheet-grip {
  width: 100%;
  height: 32px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  border-radius: 0;
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
  position: relative;
  display: block;
}
.modal-card .sheet-grip::before,
.is-notices .sheet-grip::before {
  content: "";
  position: absolute; top: 12px; left: 50%;
  width: 44px; height: 5px; margin-left: -22px;
  border-radius: var(--radius-round);
  background: var(--c-gray-200);
}
.is-notices .sheet-grip:active::before { background: var(--c-gray-300); }

/* ── TODAY, AND WHAT IS NEXT ───────────────────────────────────────────────
   Today is the darkest thing in the grid, because it is the one cell a person
   is looking for before they read anything else. A concert day is a stamp and
   a picture already; today is an ink ring around whichever cell it lands on,
   so the two states stack rather than compete. */





/* The grid shows distribution; the footer names the concert you are waiting
   for and gives you a way to add another. */
.cal-next {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  /* The row rides on the same .held-thumb the concert rows use, and that class
     places itself into a named area — without one here it dropped into an
     implicit row below the button. Naming the areas is what lets the component
     come across unchanged. */
  grid-template-areas: "thumb copy action";
  align-items: center;
  gap: 12px;
  margin-top: var(--sp-16);
  padding-top: var(--sp-16);
  border-top: 1px solid var(--c-gray-200);
}
.cal-next-copy { grid-area: copy; }
.cal-next .btn { grid-area: action; }
/* Without a picture the copy starts at the card's edge rather than indented to
   where a thumbnail would have sat. */
.cal-next:not(:has(.held-thumb)) { grid-template-areas: "copy copy action"; }
.screen .cal-next .held-thumb { width: 52px; height: 52px; }
.cal-next-copy { margin: 0; display: grid; gap: 2px; min-width: 0; }
.cal-next-copy small {
  font-family: var(--sans); font-size: 10px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
.cal-next-copy b {
  font-family: var(--sans); font-size: 15px; line-height: 1.25;
  font-weight: 650; color: var(--c-black);
}
.cal-next .btn { min-height: 40px; padding: 0 16px; white-space: nowrap; }

/* The month's claims, inside the month they belong to. The prompt keeps its
   own shape; what changes is that it now sits under the dates it is counting
   rather than in a card above them. */
.screen .challenge p { font-size: 13px; line-height: 1.45; }
.cal-next { border-top: 0; padding-top: 0; margin-top: var(--sp-16); }
/* A date that has passed is a record; the season ahead is what the calendar is
   for. The stamp stays — you were there — at the weight of something behind
   you rather than in front. */


/* A row you hold says so once, in the same quiet label the season list uses
   for Included — the state is a fact about the row, not a badge competing
   with its actions. */
.screen .row-tag {
  display: inline-block;
  justify-self: start;
  margin-top: 6px;
  padding: 2px 7px;
  border-radius: var(--radius-round);
  background: var(--c-black);
  color: var(--c-white);
  font-family: var(--sans); font-size: 9.5px; font-weight: 650;
  letter-spacing: .09em; text-transform: uppercase;
}


/* The overlap is between faces, not in front of the first one — without this
   the leading avatar hangs 7px outside the pile and clips. */
.facepile-faces i:first-child { margin-left: 0; }

/* ---- Touch targets on the small controls ---------------------------------
   Three controls rendered under 28px tall on a 390px phone: the inline Invite
   friends in the Friends heading (15px), Contact the box office in the renewal
   block (14px) and the search field's clear (25px). A finger is about 44px.
   The hit area is a pseudo-element rather than padding, so each control keeps
   the size and baseline it was designed at and only the reachable area grows. */
.screen .friends-block .connect-btn.is-inline,
.screen .renew-actions .btn--tertiary { position: relative; }
.screen .friends-block .connect-btn.is-inline::after,
.screen .renew-actions .btn--tertiary::after {
  content: ""; position: absolute; inset: -15px -8px;
}
/* The clear keeps its width — widening it pushed the control past the right edge
   of the field — and takes the field's full height instead. A pseudo-element hit
   area does not work here: .search-field clips it. */
.screen .search-clear { flex: none; align-self: stretch; padding: 0 4px;
  align-items: center; justify-content: center; }

/* ---- How a drawn mark paints ---------------------------------------------
   The Maestro set is not all outlines: the navigation marks are compound
   filled shapes (the outline is in the path), a few are silhouettes the source
   also strokes, and the rest are stroked like the Feather marks. paintIcons
   stamps which, so the rule is a fact about the drawing rather than a guess
   from where it sits. */
svg.icon[data-paint="fill"] { fill: currentColor; stroke: none; }
svg.icon[data-paint="both"] { fill: currentColor; stroke: currentColor; }

/* ---- Icon matrix ----------------------------------------------------------
   Two groups, each a stack of labelled rows, after the Figma page: a row label
   in the left column and the marks in a track of equal columns, so the Default
   and Selected rows line up mark for mark without a table. */
.icon-matrix { display: grid; gap: 34px; }
.icon-group { display: grid; gap: 14px; }
.icon-group h3 {
  margin: 0; font-family: var(--sans); font-size: 13px; font-weight: 600;
  letter-spacing: .02em; color: var(--c-black);
}
.icon-row { display: grid; grid-template-columns: 78px minmax(0, 1fr); align-items: start; gap: 16px; }
/* The label sits level with the marks, not with the name above them, so it
   clears the 12px label line and the 8px gap under it. */
.icon-row-label {
  margin: 0; font-family: var(--sans); font-size: 12px; color: var(--c-gray-500);
  padding-top: 30px;
}
.icon-cells {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 22px 10px; margin: 0; padding: 0; list-style: none;
}
.icon-cells li { display: grid; justify-items: center; gap: 8px; text-align: center; }
.icon-cells b {
  font-family: var(--sans); font-size: 10px; font-weight: 500;
  letter-spacing: -.005em; color: var(--c-black); min-height: 12px; line-height: 12px;
}
.icon-cells .icon-plate {
  display: grid; place-items: center; width: 44px; height: 44px;
  border-radius: var(--radius-inside); background: var(--c-white);
}
.icon-cells .icon { width: 24px; height: 24px; }
.icon-cells code {
  font-family: var(--mono); font-size: 9.5px; color: var(--c-gray-500);
}
.icon-cells li:has(b:empty) code { display: none; }

/* ---- The month's claims, and the one thing to do about them ---------------
   It was a whole tappable card with a pill inside it that looked like a button
   but was not one, and when the block moved into the calendar the pill was
   dropped entirely — leaving a count with nothing to do. Now the count is the
   headline, the stubs sit beside it as the remainder, and the action is a real
   full-width button at the foot of the card.

   The number is set at the size the pass card sets it, because it is the same
   fact in a different place. */
.screen .challenge { display: grid; gap: 10px; }
.challenge-count {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 12px; row-gap: 6px;
}
.screen .challenge .challenge-count b,
.challenge-count b {
  grid-row: span 2;
  font-family: var(--sans); font-size: 44px; line-height: .85; font-weight: 650;
  letter-spacing: -.03em; color: var(--c-black);
  font-variant-numeric: tabular-nums;
}
.challenge-count em {
  align-self: end;
  font-style: normal; font-family: var(--sans);
  font-size: 13.5px; line-height: 1.25; font-weight: 500; color: var(--c-black);
}
/* A date is one word. */
.nowrap { white-space: nowrap; }
.screen .challenge-count .screen .challenge p {
  margin: 0; font-size: 12.5px; line-height: 1.4; color: var(--c-gray-500);
}
.screen .challenge-go {
  margin-top: 4px; width: 100%;
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 44px; padding: 0 18px; border: 0;
  border-radius: var(--radius-button);
  background: var(--c-black); color: var(--c-white); cursor: pointer;
  font-family: var(--sans); font-size: 13px; font-weight: 600;
  letter-spacing: 0; text-transform: none;
}
.screen .challenge-go:active { background: var(--c-gray-500); }

/* ---- One label treatment for every button --------------------------------
   The claim CTA was set in sentence case at 13/600 while every .btn was
   uppercase at 11/650 with 0.085em tracking — two button languages, one of
   them on the most important control on the screen. The CTA's treatment wins,
   because a sentence-case label is what the row copy around it is set in and
   it reads at a glance; the uppercase tracking now belongs to eyebrows and
   captions, which is what it was for. Sizes and fills are untouched. */
.screen .btn,
/* The specimens sit outside .screen, so the app changed and the design system
   went on showing the label treatment it used to have. Same rule, both places. */
.btn-scale .btn, .ctl-matrix .btn, .inline-actions .btn,
.component-board .btn, .cmp-stage .btn {
  font-family: var(--sans);
  font-size: 13px; font-weight: 600;
  text-transform: none; letter-spacing: 0;
}
.screen .own-actions .btn, .screen .renew-actions .btn { font-size: 12.5px; }

/* The benefit row's action and the inline Invite friends are buttons too, and
   were the last two still set in the uppercase they no longer share with the
   rest. Same label treatment as every other control. */
.screen .benefit-list button > span:last-child,
.screen .friends-block .connect-btn.is-inline {
  font-size: 12.5px; font-weight: 600;
  text-transform: none; letter-spacing: 0;
}
.screen .benefit-list button > span:last-child { min-height: 36px; }

/* ===========================================================================
   HOME, REBUILT
   The screen opened with a full-bleed photograph and one button on it, which
   spent the whole first view on a single concert. What a patron wants on
   arriving is: anything urgent, who they are and when it is, what is new, and
   their week. In that order, in a column, at a size that fits more than one
   thing on screen at a time.
   =========================================================================== */

.screen .home-chrome {
  display: flex; align-items: center; justify-content: space-between;
  padding: 4px 0 14px;
}
.screen .home-chrome .mark-pill { background: none; padding: 0; }
/* The lockup file is white, drawn for the photograph it used to sit on. On the
   light header it has to be the other way round; inverting the one file beats
   carrying two of them. */
.screen .home-chrome .cso-logo { filter: invert(1); height: 17px; }

/* One bar, for the kind of change you need before you leave the house. Black
   on black would be a banner; this is a card that reads as an interruption
   because of where it sits, not because it shouts. */
.screen .alert-bar {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center;
  gap: 10px; width: 100%; margin-bottom: var(--sp-24);
  padding: 13px 14px; border: 0;
  border-radius: var(--radius-container);
  background: var(--c-black); color: var(--c-white);
  text-align: left; cursor: pointer;
}
.screen .alert-bar:active { background: var(--c-gray-500); }
.alert-bar-copy { display: grid; gap: 2px; min-width: 0; }
.alert-bar-copy b {
  font-family: var(--sans); font-size: 13.5px; font-weight: 650; line-height: 1.25;
}
.alert-bar-copy > span {
  font-family: var(--sans); font-size: 12px; line-height: 1.35;
  color: rgb(255 255 255 / .72);
}
.screen .alert-bar .icon-chevron { color: rgb(255 255 255 / .6); width: 16px; height: 16px; }

.home-welcome { display: grid; gap: 2px; margin-bottom: var(--sp-24); }
.screen .home-welcome .home-date {
  margin: 0; font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
.screen .home-welcome .home-greet {
  margin: 0; font-family: var(--sans); font-size: 27px; line-height: 1.12;
  font-weight: 650; letter-spacing: -.02em; color: var(--c-black);
}

/* News in a carousel. Landscape, because the picture is a thumbnail for a story
   rather than the story — a square card at this width takes the whole screen
   and only one can be seen at once. At 244px a second card shows its edge,
   which is what tells you the row scrolls. */
.news-rail {
  display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x mandatory;
  /* The rail runs to both edges of the screen while its cards keep the column's
     left margin, so the last card can scroll off rather than stop short. 18px
     is the screen's own padding. */
  margin-inline: -18px; padding-inline: 18px;
  /* Snapping ignores padding unless you say so, and without this the browser
     scrolled the first card flush to the screen edge on load. */
  scroll-padding-inline-start: 18px;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.news-rail::-webkit-scrollbar { display: none; }
.screen .news-card {
  /* A panel, not a photograph with a caption: the headline sits at the top of
     the card, the picture is inset inside it, and the line that explains the
     story closes it at the foot. At 296 the card is most of the column with
     the next one's edge showing, which is what says the row scrolls. */
  flex: none; width: 296px; scroll-snap-align: start;
  display: grid; grid-template-rows: auto auto auto;
  gap: 14px; padding: 16px; border: 1px solid var(--c-gray-200);
  border-radius: var(--radius-container);
  background: var(--c-white); text-align: left; cursor: pointer;
}
.screen .news-card:active { background: var(--c-gray-100); }
.news-head { display: grid; gap: 4px; }
.screen .news-card img {
  width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block;
  border-radius: var(--radius-inside);
}
.news-kicker {
  font-family: var(--sans); font-size: 10px; font-weight: 650;
  letter-spacing: .08em; text-transform: uppercase; color: var(--c-gray-500);
}
/* Two lines of headline whether the headline needs them or not: the cards sit
   in a row and stretch to the tallest, and without this the slack fell into
   the middle of the short ones as a gap under the picture. */
.screen .news-card b {
  font-family: var(--sans); font-size: 17px; line-height: 1.2;
  font-weight: 650; letter-spacing: -.015em; color: var(--c-black);
  min-height: calc(17px * 1.2 * 2);
  text-wrap: balance;
}
.news-note {
  font-family: var(--sans); font-size: 12.5px; line-height: 1.35; color: var(--c-gray-500);
}

/* Season progress. A bar, a number and the badges it has earned — the three
   things a progress section is for, and nothing else in the card. */
.progress-card {
  display: grid; gap: 12px;
  padding: var(--sp-16); border-radius: var(--radius-container);
  background: var(--c-white);
}
.progress-count { display: flex; align-items: baseline; gap: 10px; }
.progress-count b {
  font-family: var(--sans); font-size: 40px; line-height: .85; font-weight: 650;
  letter-spacing: -.03em; color: var(--c-black); font-variant-numeric: tabular-nums;
}
.progress-count em {
  font-style: normal; font-family: var(--sans); font-size: 13.5px;
  font-weight: 500; color: var(--c-black);
}
/* The same meter as the Profile's classifiers, given the width of this card:
   the segments stretch, the height grows, nothing else changes. */
.classifier-meter.is-wide { display: flex; gap: 4px; }
.classifier-meter.is-wide i { flex: 1; width: auto; height: 6px; }
.progress-next {
  margin: 0; font-family: var(--sans); font-size: 12.5px; line-height: 1.4;
  color: var(--c-gray-500);
}
.progress-badges {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding-top: 12px; border-top: 1px solid var(--c-gray-200);
}
.progress-badges-label {
  font-family: var(--sans); font-size: 11px; font-weight: 600;
  letter-spacing: .07em; text-transform: uppercase; color: var(--c-gray-500);
}
.badge-strip { display: flex; gap: 6px; margin: 0; padding: 0; list-style: none; }
.badge-strip li {
  width: 30px; height: 30px; border-radius: 50%;
  display: grid; place-items: center; background: var(--c-black); color: var(--c-white);
}
.badge-strip .icon { width: 15px; height: 15px; }

/* ---- Sponsors, two tiers -------------------------------------------------
   Four equal plates said all four sponsors gave the same amount. They rank:
   two at full size carry the season, four at a smaller size sit under them.
   Same cell, two scales — the ranking is the size, not a different treatment. */
/* The wall centres its children, so a grid with no width of its own sized to
   its contents and sat inset from the column. Both tiers take the full width. */
.sponsor-logos { justify-self: stretch; width: 100%; }
.sponsor-logos--lead { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.sponsor-logos--lead li { min-height: 92px; }
.sponsor-logos--lead img { max-height: 42px; }
.sponsor-logos--lead span { font-size: 15px; }

.sponsor-logos--rest { grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 4px; }
.sponsor-logos--rest li { min-height: 58px; padding: 10px 6px; }
.sponsor-logos--rest img { max-height: 22px; }
.sponsor-logos--rest span { font-size: 9.5px; line-height: 1.2; font-weight: 600; }

/* The Profile's segmented control is the Events scope control, unchanged —
   three panels is a new arrangement, not a new component. */
.screen .profile-tabs { margin-bottom: var(--sp-16); }

/* ---- The portrait was the whole first screen ------------------------------
   A square at column width is ~350px tall on a phone, so the name, the
   memberships and the tabs under them all started below the fold. At 16:9 the
   plate still reads as a portrait and the screen gets back 150 points, which
   is what puts the segmented control in the first view. */
.screen .patron-portrait { aspect-ratio: 16 / 9; }

/* The walkthrough markers were the lemon accent, which is the one saturated
   colour in the system and was being spent on a sequence number. White on
   black reads at the same size and leaves the accent for a state that means
   something. */
.map-view .site-node[data-step]::after {
  background: var(--c-black); color: var(--c-white);
}

/* SHOWING is the one row that is happening while you are reading the screen.
   It takes the accent, which is the palette's only saturated colour and now
   has exactly one job: the state that expires tonight. Booked stays black —
   a fact about the row, not an alert. */
.screen .row-tag.is-showing {
  background: var(--accent);
  color: var(--c-black);
}

/* The design system shows the app's controls, so it takes the app's rules.
   .primary-action and the filled specimens were re-uppercased by a later rule
   and had drifted from what the screens actually render. */
#system-view .btn, #system-view .btn--primary,
#system-view .primary-action, #system-view .secondary-action {
  font-size: 13px; font-weight: 600;
  text-transform: none; letter-spacing: 0;
}
/* The icon specimen inherited the white it wears on the hero, so four of the
   five plates were a white mark on a white plate — reading as empty boxes. */
#system-view .round-btn--spec { color: var(--c-black); }
#system-view .round-btn--spec.is-disabled,
#system-view .round-btn--spec:disabled { color: var(--c-gray-300); }

/* ---- The week, with room in it -------------------------------------------
   The grid was seven numbers 34px tall with a 4px gap, and the footer sat
   straight underneath — on a week with nothing in it the whole card was a row
   of grey digits. The column is the unit now: a letter, a date, and a stack
   that holds one stamp per concert, so two on a day read as two. The card
   breathes because the stack reserves its height whether or not there is
   anything in it, and the footer is a rule's width away rather than a gap's. */
.season-cal .cal { padding: var(--sp-24) var(--sp-16) var(--sp-16); }
.cal-head { margin-bottom: var(--sp-24); }
.cal-week { margin-bottom: 10px; }
.cal-grid { gap: 6px; align-items: start; }
.cal-col {
  display: grid; justify-items: center; gap: 8px;
  padding-bottom: 2px; border-radius: var(--radius-inside);
}
.cal-col-date {
  font-family: var(--sans); font-size: 14px; line-height: 1;
  color: var(--c-gray-500); font-variant-numeric: tabular-nums;
}
.cal-col.has-concert .cal-col-date { color: var(--c-black); font-weight: 600; }
/* Today is darker, and carries a dot under the date when nothing is on it —
   without one the emphasis has nowhere to land on an empty day. */
.cal-col.is-today .cal-col-date {
  color: var(--c-black); font-weight: 650;
}
.cal-col.is-today:not(.has-concert) .cal-col-date::after {
  content: ""; display: block; width: 4px; height: 4px; margin: 5px auto 0;
  border-radius: 50%; background: var(--c-black);
}
/* The stack keeps a row's height whether it holds a stamp or not, so the seven
   columns stay on one baseline and the card does not jump between weeks. */
.cal-stack { display: grid; gap: 4px; justify-items: center; min-height: 38px; }
.screen .cal-stamp {
  width: 34px; height: 38px; padding: 0; border: 0; cursor: pointer;
  border-radius: var(--radius-stamp); overflow: hidden; background: var(--c-gray-200);
  display: block;
}
.screen .cal-stamp img { width: 100%; height: 100%; object-fit: cover; display: block; }
.screen .cal-stamp.is-past { opacity: .42; }
.screen .cal-stamp:active { opacity: .7; }
/* A week with nothing in it does not need seven empty slots of stamp height. */
.cal-grid.is-empty .cal-stack { min-height: 0; }
.cal-next { margin-top: var(--sp-24); padding-top: var(--sp-16); border-top: 1px solid var(--c-gray-200); }

/* ===========================================================================
   ONE WEIGHT, AND OPACITY FOR RANK
   The app was ranking text by weight — 650 for a title, 600 for a label, 400
   for a note — which put three ink colours and three weights on the same card
   and made the screens heavy. One weight now, and the ladder is opacity: full
   ink for the thing the row is about, 58% for what supports it, 38% for the
   labels and meta that only matter when you look for them.

   font-weight is set in 257 places across this file; overriding it by
   specificity would mean touching all of them, so one rule carries it. This is
   what !important is actually for — a deliberate, single, system-wide override
   rather than a local fight with the cascade.
   =========================================================================== */
.screen, .cmp-stage {
  --ink-2: rgb(6 6 7 / .58);
  --ink-3: rgb(6 6 7 / .38);
}
.screen, .screen *, .cmp-stage, .cmp-stage * { font-weight: 400 !important; }

/* Secondary: the line under the thing, the supporting sentence, the note. */
.screen .held-copy em,
.screen .news-note,
.screen .cal-next-copy small,
.screen .progress-next,
.screen .challenge p,
.screen .alert-bar-copy > span,
.screen .classifier-copy small,
.screen .friend-copy small,
.screen .history-list small,
.screen .settings-label,
.screen .search-empty { color: var(--ink-2); }

/* Tertiary: eyebrows, counts, field labels, the day letters — present, and
   never competing with the line they label. */
.screen .held-copy small,
.screen .news-kicker,
.screen .home-date,
.screen .section-heading span,
.screen .event-group-kind,
.screen .classifier-kind,
.screen .progress-badges-label,
.screen .cal-week i,
.screen .cal-col-date,
.screen .app-header .date,
.screen .stat-row dt,
.screen .kv-list dt,
.screen .bottom-nav button { color: var(--ink-3); }

/* The exceptions are the two places where full ink is the state: the tab you
   are on, and a day the calendar is marking. */
.screen .bottom-nav button.is-active { color: var(--c-black); }
.screen .cal-col.has-concert .cal-col-date,
.screen .cal-col.is-today .cal-col-date { color: var(--c-black); }

/* The ladder is black ink on the light screens and white ink on the dark
   ones. Without this the alert bar's second line was 58% black on black —
   still there, and unreadable. Anything that sets a dark ground redefines the
   two steps rather than opting out of the system. */
.screen .alert-bar,
.screen .pass-card,
.screen .program-bar,
.screen .event-hero,
.screen .stat-row,
.screen .stage-inner {
  --ink-2: rgb(255 255 255 / .72);
  --ink-3: rgb(255 255 255 / .52);
}

/* ---- No strokes -----------------------------------------------------------
   A card in this app is a fill on the off-white ground, not a drawn edge. The
   news card had picked up a hairline, the row actions were outlined, and two
   sections were separated by a rule. Fills and space do that work instead:
   the rules become the space they were standing in, and the row actions take
   the same quiet grey the rest of the app uses for a secondary control. */
.screen .news-card { border: 0; }
.screen .row-btn { border: 0; background: var(--c-gray-100); }
.screen .row-btn:active { background: var(--c-gray-200); }
.screen .round-action { border: 0; }
.screen .badge-ico { border: 0; }
.screen .cal-next { border-top: 0; padding-top: 0; margin-top: var(--sp-24); }
.screen .progress-badges { border-top: 0; padding-top: 4px; margin-top: 4px; }

/* ---- Harmony Pass has a mark ----------------------------------------------
   The pass was a product named in a kicker, which is how you write a field,
   not how you present a membership someone is paying for monthly. It gets a
   lockup, built the way the CSO lockup is built — a mark and two lines of
   type, one scale — so the two read as belonging to the same system with the
   orchestra plainly the senior party. The badge is the set's own
   nav-membership silhouette, not a second drawing. */
.pass-lockup { display: inline-flex; align-items: center; gap: 9px; }
.pass-lockup-mark {
  display: grid; place-items: center;
  width: 30px; height: 30px; border-radius: var(--radius-stamp);
  background: rgb(255 255 255 / .14); color: var(--c-white);
}
.pass-lockup-mark .icon { width: 17px; height: 17px; }
.pass-lockup-type { display: inline-grid; gap: 1px; }
/* The case is set here rather than inherited from whatever the lockup is
   dropped into, so the mark is the same mark on the dark card, the light offer
   and the identity page. */
.pass-lockup-type b {
  font-family: var(--sans); font-size: 15px; line-height: 1;
  font-weight: 400; letter-spacing: .015em; text-transform: uppercase;
  color: var(--c-white);
}
.pass-lockup-type em {
  font-style: normal; font-family: var(--sans); font-size: 8.5px; line-height: 1;
  letter-spacing: .22em; text-transform: uppercase; color: rgb(255 255 255 / .7);
}
/* Both pass cards are photographs, so the lockup is white on both. I wrote the
   inverse first, on the assumption that the offer card was the light one — it
   is not, and the mark went nearly black on a dark picture. */

/* The pass lockup on the identity page sits on the brand plate, which is dark,
   so it needs no inversion — but it does need the plate to centre it. */
.mark-specimens--pass { margin-top: var(--sp-16); }
.mark-specimens--pass .mark-plate { display: grid; place-items: center; }

/* ---- This season: three tiers, not two ------------------------------------
   The label and the qualifier were both 11px, four points of opacity apart —
   one tier wearing two names — and the qualifier sat inline beside the number,
   so "of 6", "Orchestra A" and "since 2019" gave the three columns three
   different widths and nothing lined up. "Years giving" wrapped where the
   others did not, which put its number a line below theirs.

   Each tier now differs in both size and ink, and the qualifier goes under the
   number rather than beside it: label, figure, qualifier, one under the other,
   and the three columns read across. */
.screen .stat-row > div { gap: 0; }
.screen .stat-row dt {
  font-family: var(--sans); font-size: 9.5px; line-height: 1.2;
  letter-spacing: .08em; text-transform: uppercase; white-space: nowrap;
  color: rgb(255 255 255 / .46);
  margin-bottom: 10px;
}
.screen .stat-row dd {
  display: grid; justify-items: start; gap: 3px;
  font-family: var(--sans); font-size: 30px; line-height: .9;
  letter-spacing: -.02em; color: var(--c-white);
  font-variant-numeric: tabular-nums;
}
.screen .stat-row dd span {
  font-size: 12px; line-height: 1.25; letter-spacing: 0;
  color: rgb(255 255 255 / .72);
}

/* The Now playing flag, off. An id selector so it beats the :has() rules that
   bring the bar back on the screens that carry it. */
#app-view[data-now-bar="off"] .program-bar { display: none; }

/* Three flags do not fit a phone-width window on one line, and the row was
   centred with no wrap — so the outer two ran off both edges of the screen,
   unreachable. It wraps now, and the gap does the work the margin was doing. */
.stage-controls { flex-wrap: wrap; gap: 8px; padding-inline: 12px; }
.stage-controls .proto-switch { margin: 0; }


/* ===========================================================================
   Tonight is one object · eyebrows are one tier · "All news" is a button
   =========================================================================== */

/* ---- The change notice moves inside the Now playing capsule ---------------
   It was a black card at the top of Home while the capsule sat at the bottom:
   two dark objects about the same concert, at opposite ends of the screen,
   both opening the same sheet. Whichever one you read first made the other
   look like a duplicate. The notice is a row of the capsule now — the capsule
   says what is on, the row under it says what changed about it.

   Carrying a second row, the capsule cannot stay a pill; a stacked pill puts
   its curve through the text. It takes Radius/Container, the same pairing the
   thumbnail already uses. */
.program-bar.has-notice {
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  border-radius: var(--radius-container);
  padding: 10px 10px 0 10px;
}
.program-main { display: flex; align-items: center; gap: 14px; }
.program-main .program-live { flex: 1 1 auto; min-width: 0; }

/* One hairline, not a second fill: a filled row inside a filled capsule reads
   as a card inside a card. The notice sits a step under the title above it. */
/* Three columns now: the alert mark, the copy, the chevron. The mark is the
   same bell the announcements control uses — the notice and the place it
   files under should not be two different symbols for one idea. */
.program-notice {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center;
  gap: 10px; width: 100%;
  margin-top: 10px; padding: 11px 4px 12px 6px;
  border: 0; border-top: 1px solid rgb(255 255 255 / .14);
  background: none; text-align: left; cursor: pointer; font: inherit;
}
.program-notice:active { background: rgb(255 255 255 / .06); }
.program-notice-copy { display: grid; gap: 2px; min-width: 0; }
.program-bar .program-notice-copy b {
  font-family: var(--sans); font-size: 12.5px; line-height: 1.25;
  color: var(--c-white);
}
.program-bar .program-notice-copy > span {
  font-family: var(--sans); font-size: 11.5px; line-height: 1.35;
  color: rgb(255 255 255 / .72);
}
.program-bar .program-notice .icon-chevron {
  width: 15px; height: 15px; color: rgb(255 255 255 / .52);
}
/* The copy is two lines and the mark belongs to the first one, so it sits on
   the headline's cap height rather than floating at the centre of the block. */
.program-bar .program-notice .icon-alert {
  width: 15px; height: 15px; flex: none;
  align-self: start; margin-top: 2px;
  color: var(--c-white);
}

/* ---- One eyebrow ----------------------------------------------------------
   Five labels of the same rank had drifted to three sizes (10, 10.5, 11px) and
   three tracking values (.07em, .08em, .1em), so the tier read differently
   depending on which screen you were on — and at .07em the uppercase was
   tight enough to close up. Uppercase at this size wants one setting. */
:is(.screen, .cmp-stage) .news-kicker,
:is(.screen, .cmp-stage) .event-group-kind,
:is(.screen, .cmp-stage) .progress-badges-label,
:is(.screen, .cmp-stage) .classifier-kind,
:is(.screen, .cmp-stage) .home-date,
.screen .patron-classifiers .classifier-kind,
.screen .home-welcome .home-date,
.screen[data-screen="home"] .home-date {
  font-size: 10.5px;
  letter-spacing: .085em;
  text-transform: uppercase;
}

/* ---- "All news" is a tertiary button --------------------------------------
   It was a brand-coloured underlined link, which spent the accent on the
   smallest thing in the row. Tertiary at the small size: a quiet grey fill
   that reads as a control and lets the heading stay the loudest thing.

   Scoped past `.section-heading button`, which sets its own background and
   would otherwise strip the fill. The 44px target moves to a pseudo-element,
   the way the other small controls here do it, so the button keeps the 32px
   height the button scale specifies. */
.screen .section-heading .btn {
  position: relative;
  min-height: 32px; min-width: 0;
  margin: -4px 0; padding: 0 12px;
  border-radius: var(--radius-button);
  font-size: 11px; letter-spacing: .03em; text-decoration: none;
}
.screen .section-heading .btn::after { content: ""; position: absolute; inset: -8px -6px; }
/* No fill. Tertiary in this system is the label alone — and the phone's ground
   is #F6F5F3, which is --c-gray-100 exactly, so a grey chip here would have
   been an invisible rectangle claiming to be a button. Ink, not brand: the
   accent belongs to the thing being announced, not to the way out of the row. */
.screen .section-heading .btn--tertiary { background: none; color: var(--ink); }
.screen .section-heading .btn--tertiary:active { background: var(--c-gray-200); }


/* ---- The stage flags take the vamos segmented control ---------------------
   They were three white capsules each holding a black filled pill, so the
   build controls were the highest-contrast thing on the page — louder than
   the prototype they sit under, and the black fill read as a primary action
   rather than as which of two states is on.

   ds-vamos answers this with a gray track and a white raised thumb: the track
   says "these two belong to one question", the thumb says which one is
   selected, and nothing shouts. Values below are lifted from ds-vamos
   (`.ds-seg`, `--seg-track` / `--seg-thumb` / `--elev-1`) rather than
   re-picked, so the two surfaces cannot drift apart by eye. Note 13px: vamos
   pills are squircles, not capsules. */
.stage-controls {
  /* Measured off a rendered .ds-seg, not read off the token declarations:
     ds-vamos declares --seg-track:#ececed and --elev-1, and a later layer in
     that file overrides both, so the values that actually paint are these. */
  --seg-track: #DBDBD4;
  --seg-thumb: #FFFFFF;
  --seg-r: 13px;
  --seg-ink: #2F2F2E;
  --seg-ink-2: #6A6A67;
  --seg-elev: 0 1px 2px -1px rgb(24 24 24 / .16), 0 4px 10px -3px rgb(24 24 24 / .13);
  gap: 18px;
}
/* The outer capsule goes: a track inside a pill was a container inside a
   container, and the label does not need a chip to sit on. */
.stage-controls .proto-switch {
  background: none; border-radius: 0; padding: 0;
  gap: 10px;
}
.stage-controls .proto-switch-set {
  display: inline-flex; gap: 2px;
  padding: 3px;
  border-radius: var(--seg-r);
  background: var(--seg-track);
}
.stage-controls .proto-switch-set button {
  min-height: 0; padding: 8px 14px;
  border: 0; cursor: pointer;
  border-radius: calc(var(--seg-r) - 3px);
  background: transparent; color: var(--seg-ink-2);
  font-family: var(--sans); font-size: 13px; font-weight: 500; line-height: 1;
  transition: background 160ms ease, color 160ms ease;
}
.stage-controls .proto-switch-set button.is-active {
  background: var(--seg-thumb); color: var(--seg-ink);
  box-shadow: var(--seg-elev);
}
