/* Generated by script/build_tokens.rb from design/tokens.json. Do not edit.
 *
 * Regenerate:  ruby script/build_tokens.rb
 * Verify:      ruby script/build_tokens.rb --check
 */

:root {
  /* Brand green. The baseline also contained #1aaf11 in four places — a 2-point difference in the blue channel, visually identical, treated here as a typo and unified. */
  --gp-color-green: #1aaf13;
  /* Was --dk-green. Form control text, progress borders. */
  --gp-color-green-dark: #1a8754;
  /* Border paired with green on the checkpoint buttons. */
  --gp-color-green-border: #198510;
  --gp-color-white: #ffffff;
  --gp-color-black: #000000;
  /* Body background. */
  --gp-color-page: #fefefe;
  /* Chud checkpoint banner. */
  --gp-color-alert-yellow: #ffff00;
  /* Bootstrap's .alert-secondary background, which is what a sale notification has always been painted. Kept as the opaque fill for floating surfaces that must not let anything through — see surface/plate. */
  --gp-color-plate: #e2e3e5;
  /* Bootstrap's --bs-danger, and the border an invalid form control already gets today: .form-control.is-invalid, applied by bootstrap_form whenever the record has an error on that attribute. */
  --gp-color-red: #dc3545;
  /* The text colour of .alert-danger in the vendored Bootstrap — the error summary shared/_errors renders. Also ships today; used here for error text because #dc3545 is 4.49:1 on the page background, a hair under AA. */
  --gp-color-red-dark: #842029;
  /* Bootstrap's .bg-light. Recorded because it is what the chud status card actually renders: baseline:285 paints the card with surface/glass, and .bg-light on the same element carries !important and wins. The card reads as glass in the CSS and ships opaque. */
  --gp-color-bootstrap-light: #f8f9fa;
  /* Bootstrap's .border. The stat chips carry .border, whose !important beats the white hairline at baseline:176 — so the chip border is this grey, not border/glass. 1.48:1 on the page background, which fails WCAG 1.4.11; kept because it is what ships, not because it is right. */
  --gp-color-bootstrap-border: #dee2e6;
  /* Bootstrap's .text-muted. The stat chip label carries .text-muted, whose !important beats the #6c6c6c at baseline:186 — so grey-700 is NOT the colour that label renders, despite its description. Importance beats specificity. */
  --gp-color-bootstrap-muted: #6c757d;
  /* Bootstrap's --bs-body-color, inherited by <body>. Nothing in Setting[custom_css] sets a text colour on the attendee body, so this — not text/body's black — is what every ordinary paragraph and heading on the attendee surface actually paints. Measured on /vouchers at 390px: the h1 and the blurb both compute rgb(33, 37, 41). */
  --gp-color-bootstrap-body: #212529;
  /* Bootstrap's .form-control border. Distinct from bootstrap-border (#dee2e6, the .border utility) and it is the one a text input actually gets. No page block in the row restyles the border on .form-control, so all four form pages ship this grey hairline — measured rgb(206, 212, 218) on /vouchers. The Figma Field component draws border/glass instead, which is the value .gp-field__control was reconciled AWAY from; that component is wrong and is tracked separately. */
  --gp-color-bootstrap-input-border: #ced4da;
  --gp-color-grey-100: #ededed;
  /* Light stop of the stat chip's gradient (baseline:166-170); grey-200 is the dark stop. */
  --gp-color-grey-150: #e6e6e6;
  --gp-color-grey-200: #dcdcdc;
  --gp-color-grey-300: #d0d0d0;
  --gp-color-grey-400: #cbcbcb;
  --gp-color-grey-600: #6e6e6e;
  /* Muted label text. */
  --gp-color-grey-700: #6c6c6c;
  /* Label text in #shop (baseline:894). NOT what the three form pages ship — their labels inherit Bootstrap's body colour, #212529 — so .gp-field__label deliberately sets no colour. */
  --gp-color-grey-800: #3c3c3c;
  --gp-color-glass-fill: rgba(255, 255, 255, 0.65);
  --gp-color-glass-fill-strong: rgba(255, 255, 255, 0.78);
  --gp-color-glass-fill-faint: rgba(255, 255, 255, 0.15);
  /* The login form controls (baseline:730 — pre-cut snapshot; the #login block was removed from the row 2026-09-20, so that line number has no successor and now addresses an unrelated rule), and the only glass fill in the row that is not one of the three above. Thinner than glass-fill on purpose: those fields sit directly on the sky photograph and the type inside them is green-dark, so the plate has to read as glass rather than as a panel. */
  --gp-color-glass-fill-soft: rgba(255, 255, 255, 0.4);
  --gp-color-glass-border: rgba(255, 255, 255, 0.75);
  --gp-color-glass-border-bright: rgba(255, 255, 255, 0.8);
  --gp-color-glass-sheen: rgba(255, 255, 255, 0.1);
  /* Avatar chip. Hue 175°, 5.47:1 against white text. 58° from the nearest character hue. */
  --gp-color-avatar-teal: #0f766e;
  /* Gradient top-left stop for avatar-teal: the base mixed 30% toward white. */
  --gp-color-avatar-teal-light: #579f9a;
  /* Avatar chip. Hue 201°, 5.93:1 against white text. 84° from the nearest character hue. */
  --gp-color-avatar-ocean: #0369a1;
  /* Gradient top-left stop for avatar-ocean: the base mixed 30% toward white. */
  --gp-color-avatar-ocean-light: #4f96bd;
  /* Avatar chip. Hue 263°, 7.10:1 against white text. 43° from the nearest character hue. */
  --gp-color-avatar-violet: #6d28d9;
  /* Gradient top-left stop for avatar-violet: the base mixed 30% toward white. */
  --gp-color-avatar-violet-light: #9968e4;
  /* Avatar chip. Hue 345°, 6.29:1 against white text. 39° from Bachman's mauve — the tightest gap in the palette, and the only slot the hue circle leaves between Bachman at 306° and AMJ at 31°. */
  --gp-color-avatar-rose: #be123c;
  /* Gradient top-left stop for avatar-rose: the base mixed 30% toward white. */
  --gp-color-avatar-rose-light: #d25976;
  /* Avatar chip, neutral. 10.35:1 against white text. */
  --gp-color-avatar-slate: #334155;
  /* Gradient top-left stop for avatar-slate: the base mixed 30% toward white. */
  --gp-color-avatar-slate-light: #707a88;
  /* Avatar chip, pale — the only one carrying DARK text (color/black, 16.73:1). It exists because the hue circle is exhausted: the characters own orange, green and mauve, leaving only 152-271° and a sliver near 345°. A pale chip differs in lightness rather than hue so it cannot collide with any character colour, present or future. It needs a dark ring: it sits 1.26:1 against white. */
  --gp-color-avatar-bone: #e7e5e4;
  /* Gradient top-left stop for avatar-bone. NOT the 30%-toward-white mix the other five stops use: that formula gives #eeedec, which is indistinguishable from the #e7e5e4 base it is supposed to lighten because bone starts only a few percent short of white already. Chosen by hand instead, a few steps further up the same near-white run. */
  --gp-color-avatar-bone-light: #fafaf9;
  /* The top of the /login sky. Measured, not picked: the average of the top 1% of app/assets/images/gp-sky-portrait.jpg, which is #273b7f, and of gp-sky-landscape.jpg, which is the same to the byte. It is .gp-sky's background-COLOR under the photograph, and it has two jobs -- the colour the page holds while the JPEG is still arriving, and the one iOS 26 Safari tints the status bar with. Safari never reads background images, gradients or pseudo-elements; with viewport-fit=cover and no fixed element at the top edge it falls back to the body's background-color, which is this. Without it the sky runs under a WHITE status bar. */
  --gp-color-sky-top: #273b7f;
  /* 8 uses in the baseline. */
  --gp-dimension-radius-card: 16px;
  /* Navbar capsule. */
  --gp-dimension-radius-nav: 35px;
  /* Was 5rem. 9 uses. Any value past half the control height rounds identically, so px is safe here. */
  --gp-dimension-radius-pill: 80px;
  --gp-dimension-radius-round: 999px;
  /* 6 uses. Buttons, form controls, balance pill. */
  --gp-dimension-control-height: 42px;
  /* Shop inputs and selects. */
  --gp-dimension-control-height-sm: 35px;
  /* Stat chip (baseline:172). Fixed, so a long level name wraps out of it rather than growing it. */
  --gp-dimension-chip-height: 60px;
  /* Level progress bar (baseline:247). Fixed px inside a fluid card — it can overflow a narrow one. Recorded as a parity defect to carry, not a value to reuse. */
  --gp-dimension-meter-width: 300px;
  /* Form column on login, vouchers, edit-info and referrals. */
  --gp-dimension-form-width: 350px;
  --gp-dimension-blur-glass: 2px;
  /* The navbar's OWN measured height/offset, WITHOUT any safe-area inset. Measured live at 390x844, signed in, with script/measure_live.js: nav.navbar's getBoundingClientRect() bottom edge is 79.1875px (top: 10px, height: 69.1875px; the navbar is position: relative, not fixed, so this only matters unscrolled). Rounded up for breathing room. Consumed by the mobile sale toast's top offset (sale-toast-layer branch) as calc(env(safe-area-inset-top) + var(--gp-dimension-navbar-clearance)) -- consumers MUST add env(safe-area-inset-top) themselves. This token cannot include it: it was measured in headless Chrome, where every env() reads 0, and PR #209 (main) later added viewport-fit=cover, so a real notched phone pushes the navbar down by that inset and a toast pinned to the bare token alone would sit back on the navbar-brand wordmark -- the bug this token exists to prevent. */
  --gp-dimension-navbar-clearance: 80px;
  /* The gap under the login form, between the Join Show button and the footer (Corey, 2026-09-21: the button sat too close). Was 30px, a literal on `#login form`. Not a general spacing step -- this file has no spacing scale and this is the one measured gap. */
  --gp-dimension-form-gap-bottom: 72px;
  /* Above the logged-out logo on /login, on top of the safe-area inset. Was 40px when first added 2026-09-21; tightened to 24px the same day, because the whole page has to fit a phone without scrolling and the logo's gap was the cheapest 16px to find. */
  --gp-dimension-logo-gap-top: 24px;
  /* The gap between a .gp-toast-stack and the edges of the viewport, and between the stack and the top of the screen. Stacked ON TOP of env(safe-area-inset-*) rather than instead of it: the layout carries viewport-fit=cover, so on a notched iPhone the status bar inset is added to this and on a desktop this is the whole margin. 16px, matching the 1rem the other fixed box on this surface uses (#notifications' .p-3, application.css) -- the two are pinned to opposite corners and should read as the same margin. */
  --gp-dimension-toast-inset: 16px;
  /* Between two stacked .gp-toast alerts. All four /login arrival messages are mutually exclusive, so two toasts only ever stack when a controller sets a notice alongside an alert; the value matters anyway because the stack is a flex column and Bootstrap's .alert margin-bottom is overridden in it. */
  --gp-dimension-toast-gap: 8px;
  /* The vertical rhythm between the logo, the operator's login_info copy and the form on /login. Replaces Bootstrap's .my-3 (16px) and the row's .mt-5 (16px) on that page only. Corey, 2026-09-21: 'less space between logo and that so the whole form fits without scrolling'. Measured: the document was 883px against an 844 viewport, and a real iPhone loses ~90px more to Safari's chrome. */
  --gp-dimension-login-stack-gap: 8px;
  /* ATTENDEE UI ONLY. Corey, 2026-09-14: the stage screen and broadcast board use Grange and Richmond and no IBM Plex Sans at all; the attendee UI is all IBM Plex Sans. Do not bind board or stage text to this token — they are two type systems, not one with an exception. The repository DOES load IBM Plex Sans now, as of 2026-09-15: app/assets/stylesheets/fonts.css declares the twelve faces and serves the woff2 files from app/assets/fonts, linked into the three attendee layouts and deliberately not into admin. Before that, the only loader was line 1 of the operator's custom_css row, which meant emptying the row would have taken the typeface off every attendee page in the same motion. The row's @import still resolves later in the cascade and still wins while the row is live, so fonts.css is inert WHILE THAT LOADER SUCCEEDS -- which is narrower than inert and is the point. When the @import or one of its font resources fails, those faces are not merely outranked, they do not exist, and the local ones are selected instead: the page then renders IBM Plex Sans where today it falls through to Helvetica Neue. That is a rendering difference from today, in exactly the bad-wifi scenario the file exists for. Emptying the row is the full handover. Regenerate it with script/fetch_webfont.rb, which derives every descriptor from that same @import rather than letting anyone author one. The four emoji families are NOT optional padding: the operator's row sets --bs-font-sans-serif to exactly this ten-entry stack (baseline:6), and admin's --gpa-font-family-base carries the same ten. This token shipped with six, dropping them, which broke the project's own rule that a token models what ships. It was inert -- nothing binds var(--gp-font-family) -- so the fix changes no rendering, but binding a six-entry stack during the migration would have quietly removed emoji coverage from every attendee page. */
  --gp-font-family: "IBM Plex Sans", "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
  /* TWO NAMES ON PURPOSE. Lowercase "grange" is the Adobe web project (https://use.typekit.net/wsd4utv.css), which renames every family to its own slug; capital "Grange" is the CoreSync install on a signed-in Mac. Neither alone covers both machines, so both are in the chain with the slug first. Verified against the served CSS: it defines grange, grange-condensed and grange-extended at weights 400 and 700 only, normal and italic. NO HEAVY over the link — asking for 800 gets a synthesised bold, and on a CoreSync machine gets a REAL Grange Heavy, so the same page would render differently depending on whether the venue is signed into Creative Cloud. Pin stage weights to 700. The link is loaded on performers/status only, never in minimum.html.erb, which is shared with /order_notifications. Stage only; never an attendee surface. */
  --gp-font-family-grange: grange, Grange, "Helvetica Neue", Arial, sans-serif;
  /* Condensed is a SEPARATE FAMILY over the web link, not a width axis — font-stretch: condensed does nothing against the Typekit font and you must name "grange-condensed". The second name, capital Grange, is the CoreSync install, where font-stretch DOES select the condensed cut, so a declaration wanting condensed should set this family AND font-stretch: condensed. Needed because the board's big numbers overflow at normal width: the mockup sets a four-digit total 84px wide at 60px and Grange Bold is nearer 140px. */
  --gp-font-family-grange-condensed: grange-condensed, Grange, "Helvetica Neue", Arial, sans-serif;
  /* NOT IN THE WEB PROJECT. https://use.typekit.net/wsd4utv.css serves only the three Grange families — zero Richmond of any kind — so unlike Grange this resolves ONLY on a machine with Richmond Display installed locally through CoreSync, and falls back to Helvetica everywhere else, including over the link. A different situation from Grange, and the reason this token is riskier than it looks. The family is "Richmond Display", NOT "Richmond": system_profiler SPFontsDataType reports Family: Richmond Display with 14 styles. This token said "Richmond" until 2026-09-14 and could never have matched. Nothing binds it yet. */
  --gp-font-family-richmond: "Richmond Display", "Helvetica Neue", Arial, sans-serif;
  --gp-font-weight-regular: 400;
  --gp-font-weight-medium: 500;
  --gp-font-weight-semibold: 600;
  --gp-font-weight-bold: 700;
  --gp-font-weight-heavy: 800;
  /* DEAD AS SHIPPED — kept because the value is real in the row and deleting it would hide that. All three uses are `.form-label { font-size: 0.6rem }` inside #login, [id$="-edit"] and #referrals-new, and every one of them is beaten by `.mb-3 label { font-size: 0.75rem }` in the same block: (0,1,1,1) over (0,1,1,0). bootstrap_form wraps every non-inline field in .mb-3, so a form label on those pages renders at 0.75rem and this token has never painted a pixel. .gp-field__label uses font/size-small. Delete this and the matching Figma variable after 4 October. */
  --gp-font-size-label: 0.6rem;
  /* Stat chip label (baseline:191). Deliberately px, not rem: the baseline sets it in px and 9px is below the point where a rem conversion is faithful. Smaller than any sane minimum — recorded as shipped, not endorsed. */
  --gp-font-size-micro: 9px;
  /* Level NAME in the first stat chip (baseline:236), which is smaller than the numeric values beside it because the names run long — "Senior Product Pioneer" at 24px would not fit 30% of a phone width. */
  --gp-font-size-chip: 13px;
  --gp-font-size-small: 0.75rem;
  --gp-font-size-body: 0.875rem;
  /* Checkpoint banner, participation body copy. */
  --gp-font-size-lead: 1.125rem;
  /* Product name, panel headers. */
  --gp-font-size-h2: 1.5rem;
  /* Page titles. */
  --gp-font-size-h1: 2rem;
  /* Broadcast board only. Read at distance, never on a phone. */
  --gp-font-size-display: 6rem;
  --gp-shadow-glass: 0px 8px 32px 0px rgba(31, 38, 135, 0.2), inset 0px 4px 20px 0px rgba(255, 255, 255, 0.3);
  --gp-shadow-card: 0px 2px 6px 0px rgba(0, 0, 0, 0.08), inset 0px 1px 2px 0px rgba(0, 0, 0, 0.06);
  /* The ::after highlight. Written 16 times in the baseline, but only 11 attach to an element that can render a pseudo-element (baseline:704 — pre-cut snapshot; the #login block was removed from the row 2026-09-20, so that line number has no successor — and :951 target <input>, and the three .btn-primary::after copies target submit inputs), and 2 of the survivors use drop-shadow(10px 3px 6px #888) brightness(122%) instead. All 11 carry an IDENTICAL box-shadow — the 2 variants differ only in their filter chain and a misspelt --backdrop-filter — so THIS token has 11 live copies. The wider pseudo-element recipe has 9. Still the most duplicated thing in the file. */
  --gp-shadow-rim: inset -10px -8px 0px -11px rgba(255, 255, 255, 1), inset 0px -9px 0px -8px rgba(255, 255, 255, 1);
  --gp-zIndex-reminder: 1040;
  --gp-zIndex-overlay: 1050;
  /* spec/views/takeover_layering_spec.rb pins the ladder: above the checkpoint (1065), below being on stage (1075). It never has to rank against the quiz or being chosen/standby — those suppress the toast outright with :has() next to #notifications in application.css, because nobody can buy anything while either owns the screen. */
  --gp-zIndex-sale-toast: 1070;
  /* The flash toast stack. 1050 DELIBERATELY: the same tier as #notifications (shared/_notifications.html.erb), which is under every takeover -- the quiz (1055), being chosen or on standby (1060, also the welcome modal), the checkpoint (1065) and being live on stage (1075). A login toast cannot meet a takeover today (all five need a signed-in attendee and /login has none), but issue #215 moves the rest of the attendee surface onto this component, and a toast that outranked a takeover would cover a screen the show has deliberately made inescapable. Ranking it with the other non-blocking notifications is the answer that stays right once it is used elsewhere. */
  --gp-zIndex-toast: 1050;
  --gp-surface-page: var(--gp-color-page);
  --gp-surface-glass: var(--gp-color-glass-fill);
  --gp-surface-glass-strong: var(--gp-color-glass-fill-strong);
  /* A control sitting on photography rather than on the page background. Only #login uses it today. */
  --gp-surface-glass-soft: var(--gp-color-glass-fill-soft);
  --gp-surface-alert: var(--gp-color-alert-yellow);
  /* An opaque floating surface. Glass in shape — same radius, rim and shadow — but solid, for anything that sits over video or over content it must not reveal. The sale toast is the case that forced it: /order_notifications is a transparent browser source, so any alpha at all lets the footage through. */
  --gp-surface-plate: var(--gp-color-plate);
  /* The solid dark block — the +N Chuds chip on a sale toast. */
  --gp-surface-ink: var(--gp-color-black);
  /* The unfilled part of a meter. On the broadcast board the accent fill grows along this as chuds arrive. */
  --gp-surface-track: var(--gp-color-white);
  --gp-text-body: var(--gp-color-black);
  --gp-text-muted: var(--gp-color-grey-700);
  --gp-text-label: var(--gp-color-grey-800);
  --gp-text-field: var(--gp-color-green-dark);
  --gp-text-on-dark: var(--gp-color-white);
  /* Text over photography — the login cloud plate, the broadcast board. Same value as on-dark today but a different contract: on-dark is safe by construction, on-image is only safe with a scrim behind it. */
  --gp-text-on-image: var(--gp-color-white);
  /* Validation message under a field. Darker than border/error on purpose: #dc3545 is 4.49:1 on the page background and this is small text, #842029 is 9.38:1. Same reasoning as border/focus being black. */
  --gp-text-error: var(--gp-color-red-dark);
  --gp-action-primary: var(--gp-color-green);
  --gp-action-primary-border: var(--gp-color-green-border);
  --gp-action-on-primary: var(--gp-color-white);
  /* performers.icon_color defaults to #FFFFFF, which is invisible against surface/track and against white text. Anything rendering an accent must fall back to this when icon_color is blank or white. */
  --gp-accent-fallback: var(--gp-color-green);
  /* Bachman's character colour. The runtime value comes from performers.icon_color on the record, NOT from here — accent_colour in app/helpers/performers_helper.rb renders the database string directly. Recorded because the avatar palette is defined by its hue distance from the three character colours, and a distance to an untokenised literal is not reviewable. Source: the bound Figma variable on stage board frame 102:253. */
  --gp-accent-bachman: #A1659B;
  /* AMJ's character colour, and the reason the avatar palette carries no orange. Runtime value comes from performers.icon_color. NOTE: Figma frame 131:646 shows #FF9500 for AMJ — that is iOS system orange from a community kit and is wrong; the bound variable on 102:253 is authoritative. */
  --gp-accent-amj: #F4A045;
  /* Todd's character colour. Runtime value comes from performers.icon_color. Sits 23° from color/green, which is why no green entered the avatar palette. */
  --gp-accent-todd: #76BA43;
  --gp-border-glass: var(--gp-color-glass-border);
  --gp-border-glass-bright: var(--gp-color-glass-border-bright);
  /* Focus ring. Black rather than brand green on purpose: a focus indicator needs 3:1 against BOTH the control it outlines and the surface behind it, and green-dark against green is nowhere near that. Black is 4.9:1 on the green button and 21:1 on the page. */
  --gp-border-focus: var(--gp-color-black);
  /* Border of an invalid control. Matches what .form-control.is-invalid already paints, so a .gp-field and a Bootstrap field in the same form do not disagree about what invalid looks like. */
  --gp-border-error: var(--gp-color-red);
  --gp-radius-pill: var(--gp-dimension-radius-pill);
  --gp-radius-card: var(--gp-dimension-radius-card);
  --gp-radius-nav: var(--gp-dimension-radius-nav);
  --gp-control-height: var(--gp-dimension-control-height);
  --gp-control-height-sm: var(--gp-dimension-control-height-sm);
  /* A button sinking as a finger presses (checkpoint-ui-polish, round 8, Corey: press and hover with colour and motion). Quick, so the press is felt at once. */
  --gp-motion-press: 120ms;
  /* A button coming back up after a press, on an overshooting curve (components.css) -- the light bounce of a spring, in CSS. */
  --gp-motion-press-release: 380ms;
}
