/**
 * The public marketing site (bluegrassfireworks.com) — Chad: "I like how
 * this website is setup [bigbangfireworks219.com] lets take aspects and
 * design chices from them, this does not need to match the mailer design
 * or the dashboard design, this is completley different." A fresh, original
 * palette/type system (not a reskin of shop.css or app's own dashboard
 * styles) borrowing the reference site's *structure* — dark hero, pill
 * badges, a promo strip, category cards, a stats strip — not its content.
 *
 * Single fixed dark identity, deliberately no light-mode variant — a
 * branded marketing site, not a tool the viewer's own theme should bleed
 * into.
 */

:root {
  /* Night-sky navy rather than a plain near-black — a warmer, deeper base
     than the dashboard/mailer's own neutrals, with room for a gold spark
     and an ember-red pop to read as fireworks without going patriotic-cliché. */
  --sky-950: #070a16;
  --sky-900: #0c1226;
  --sky-800: #131b36;
  --sky-700: #1c2748;
  --sky-600: #2b3a63;
  --line: rgba(255,255,255,.10);
  --ink: #f4f1ea;
  --ink-dim: #b9c0d6;
  --ink-faint: #7d86a3;
  /* Chad tried red, then cobalt blue, then asked to see red and blue
     variations side by side against the real header/hero — landed on
     Steel Blue, the most muted/desaturated of the eight. --spark is the
     one variable every accent-colored thing on the site actually reads
     from (buttons, headings, prices, active nav, etc.), so trying a new
     accent is just this one edit. --ember stays its own fixed red
     regardless (the safety-page disclaimer, the mailer pill) — a real
     red "pop" reads as an intentional warning/CTA color again once the
     primary accent isn't red itself. --caution is the banner's own,
     deliberately separate, fixed yellow (kept at the original --spark
     values under a name nothing else points at) — "lets not change the
     top banner though, it stays black and yellow," no matter what the
     rest of the site's accent does, now or later. */
  --spark: #2c5aa0;
  --spark-bright: #5c8bd6;
  --ember: #e0524a;
  --ember-bright: #ff6b5e;
  --caution: #f6b93b;
  --caution-bright: #ffd166;
  --ok: #4fbf8b;

  /* Chad: "New thickness of font looks packed and unreadable, show me
     some font options to pick" — Anton read too cramped once headings
     started leaning on it more heavily (the Safety page's info cards,
     checklist, etc.). Replaced with Bungee after a live side-by-side
     comparison across every size this variable is actually used at. */
  --display: 'Bungee', 'Arial Black', sans-serif;
  --head: 'Barlow Condensed', 'Barlow', sans-serif;
  --body: 'Barlow', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  --wrap: 1180px;
  --radius: 14px;
}

* { box-sizing: border-box; }
/* Chad: "The mobile version sidebar shows a zoomed window that must be
   scrolled." Confirmed: on a real mobile viewport, window.innerWidth
   was reporting double the device width (e.g. 750 instead of 375) —
   something briefly wider than the screen during first paint (before
   fonts/layout settle) was enough for the phone's own viewport
   calculation to lock onto that wider width and never come back down,
   even once every element measured correctly afterward. overflow-x:
   hidden on the two elements that actually scroll the page is the
   standard, robust fix — it can't stop something from being wide for
   an instant, but it stops that instant from ever being scrollable/
   zoomable in the first place. */
html { scroll-behavior: smooth; overflow-x: hidden; }
body {
  margin: 0;
  background: var(--sky-950);
  color: var(--ink);
  font: 400 16px/1.55 var(--body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
h1, h2, h3, h4 { margin: 0; font-family: var(--head); font-weight: 700; text-wrap: balance; }
p { margin: 0; }
a { color: inherit; }
img { max-width: 100%; display: block; }
button { font: inherit; }

.ps-loading, .ps-error {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  color: var(--ink-dim); font-family: var(--head); font-size: 1.3rem; letter-spacing: .03em; text-align: center; padding: 2rem;
}

.ps-wrap { max-width: var(--wrap); margin: 0 auto; padding: 0 1.5rem; }

/* ---------------- header / nav ---------------- */
/* ---------------- caution-tape age banner ---------------- */
/* Chad: "a thin banner at the top of the website that looks like caution
   tape with a fireworks disclaimer saying 18+ and linking to the safety
   page." Real diagonal hazard stripes (a repeating-linear-gradient, not
   an image) so it stays crisp at any width and never needs an asset. */
.ps-caution {
  background: repeating-linear-gradient(45deg,
    var(--caution) 0, var(--caution) 18px, var(--sky-950) 18px, var(--sky-950) 36px);
}
.ps-caution-inner {
  max-width: var(--wrap); margin: 0 auto; padding: .4rem 1.5rem;
  display: flex; justify-content: center;
}
.ps-caution a {
  display: inline-block; background: var(--sky-950); color: var(--caution-bright);
  text-decoration: none; font-family: var(--head); font-weight: 700;
  font-size: .82rem; letter-spacing: .03em; text-transform: uppercase;
  padding: .3rem .9rem; border-radius: 999px;
}
.ps-caution a:hover { color: #fff; }
.ps-caution a .age { color: var(--ember-bright); }

.ps-header {
  position: sticky; top: 0; z-index: 20;
  /* Chad: "The mobile version sidebar shows a zoomed window that must
     be scrolled." Root cause: backdrop-filter creates a new containing
     block for any position:fixed descendant (real, spec-defined
     behavior in every engine, not a rendering quirk) — the mobile nav
     panel (position:fixed, a descendant of this header) was sizing
     itself against this ~64px-tall header instead of the viewport, so
     "full screen" collapsed to a sliver with everything below it
     needing a scroll to reach. Dropped the blur; the background alone
     is opaque enough not to need it. */
  background: rgba(7,10,22,.94);
  border-bottom: 1px solid var(--line);
}
.ps-header-inner {
  max-width: var(--wrap); margin: 0 auto; padding: .85rem 1.5rem;
  display: flex; align-items: center; gap: 1.5rem;
}
.ps-brand { display: flex; align-items: center; text-decoration: none; margin-right: auto; }
/* The real logo (navy "BLUEGRASS" + horse-head + red "FIREWORKS", fully
   transparent PNG) is navy ink on a near-black header — unreadable with no
   treatment. logo_style (a public_site setting, see src/lib/public-site.js)
   picks one of these three; confirmed live against the actual header
   before picking a default. Each is CSS-only — no extra logo assets. */
.ps-brand img { height: 40px; width: auto; }
/* outline: thin stroke traced around the logo's silhouette via stacked
   drop-shadows (there's no native CSS "stroke" for a raster image) — keeps
   the real navy/red colors, just adds contrast against the dark header. */
.ps-logo-outline {
  filter:
    drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff)
    drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)
    drop-shadow(1px 1px 0 #fff) drop-shadow(-1px -1px 0 #fff)
    drop-shadow(1px -1px 0 #fff) drop-shadow(-1px 1px 0 #fff);
}
/* knockout: the whole logo recolored solid white — brightness(0) crushes
   every opaque pixel to black (alpha untouched), invert(1) flips it white. */
.ps-logo-knockout { filter: brightness(0) invert(1); }
/* gold-outline: same stroke technique as outline, tinted to the site's own
   caution-gold instead of white. */
.ps-logo-gold-outline {
  filter:
    drop-shadow(1px 0 0 var(--caution)) drop-shadow(-1px 0 0 var(--caution))
    drop-shadow(0 1px 0 var(--caution)) drop-shadow(0 -1px 0 var(--caution))
    drop-shadow(1px 1px 0 var(--caution)) drop-shadow(-1px -1px 0 var(--caution))
    drop-shadow(1px -1px 0 var(--caution)) drop-shadow(-1px 1px 0 var(--caution));
}
.ps-nav { display: flex; gap: 1.6rem; align-items: center; }
.ps-nav a {
  text-decoration: none; color: var(--ink-dim); font-family: var(--head); font-weight: 600;
  font-size: .98rem; letter-spacing: .02em; text-transform: uppercase; padding: .3rem 0;
  border-bottom: 2px solid transparent; transition: color .15s, border-color .15s;
}
.ps-nav a:hover, .ps-nav a.active { color: var(--spark-bright); border-color: var(--spark); }
.ps-nav-mailer {
  /* Chad: "'shop your mailer' is still red" — was hardcoded to --ember
     (a fixed red, independent of the site's own accent) from before
     there was an accent to follow at all. Follows --spark like every
     other accent-colored thing on the site now, instead of its own
     fixed color. */
  background: var(--spark); color: #fff !important; padding: .5rem 1rem !important;
  border-radius: 999px; border-bottom: none !important;
  /* At in-between widths — narrower than a full desktop layout but still
     above the 780px breakpoint that switches to the hamburger menu — the
     header row could run short on space and let "Shop Your Mailer" wrap
     to two lines, stretching the pill's fixed 999px radius into a tall
     oval instead of a pill. nowrap keeps it one line; the row itself
     already has room to give (the nav links can shrink first). */
  white-space: nowrap;
}
.ps-nav-mailer:hover { background: var(--spark-bright); }
/* "My Account (Firstname)" can run longer than any other nav label —
   same overflow concern .ps-nav-mailer's own comment already flags for
   "Shop Your Mailer" at in-between widths. */
.ps-nav-account { white-space: nowrap; }
.ps-nav-toggle { display: none; }

@media (max-width: 780px) {
  /* The open menu was collapsing to a ~64px sliver instead of covering
     the screen — .ps-nav's own desktop rule sets align-items:center
     (for vertically centering inline links in the header row) and
     nothing here overrode it back for the vertical panel; height/width
     set explicitly alongside inset, not relying on inset alone, since
     that's what actually fixed it.
     --nav-top is set from JS (wireNav, positionNav) to the real height
     of the banner + header stacked above it, not a hardcoded guess —
     the caution banner didn't exist when 60px was first written here,
     and can wrap to two lines on a narrow phone anyway, so any fixed
     number here would just be the next version of the same bug. */
  .ps-nav { position: fixed; top: var(--nav-top, 60px); right: 0; bottom: 0; left: 0;
    width: 100%; height: calc(100% - var(--nav-top, 60px));
    background: var(--sky-950); flex-direction: column; align-items: stretch; text-align: center;
    justify-content: flex-start; padding: 2rem 1.5rem; gap: 1.4rem; transform: translateX(100%);
    transition: transform .2s ease; overflow-y: auto; }
  .ps-nav.open { transform: translateX(0); }
  .ps-nav a { font-size: 1.2rem; }
  .ps-nav-toggle {
    display: block; background: none; border: 1px solid var(--line); color: var(--ink);
    border-radius: 8px; padding: .45rem .7rem; font-size: 1.1rem; line-height: 1;
  }
}

/* ---------------- hero ---------------- */
.ps-hero {
  position: relative; overflow: hidden;
  padding: 5.5rem 1.5rem 4rem;
  background:
    radial-gradient(60% 60% at 50% 0%, rgba(246,185,59,.16), transparent 70%),
    radial-gradient(45% 45% at 85% 15%, rgba(224,82,74,.14), transparent 70%),
    var(--sky-950);
  text-align: center;
}
/* Chad: "the vision is to have the background of this with fireworks
   going off similar to the style shown in the 3d preview" — an original
   canvas particle system (heroFireworks in public-site.js), not anyone
   else's asset.
   A negative z-index was the first approach here and turned out to be
   the actual bug behind "I never saw a single firework" — even a
   manually-drawn, permanently-static test shape confirmed present in
   the canvas's own pixel buffer (getImageData) never actually painted
   on screen, in either a screenshot or Chad's own real browser. Canvas
   elements are commonly promoted to their own GPU compositing layer,
   and z-index:-1 relative to a plain position:relative ancestor is a
   known-unreliable way to keep such a layer painting where intended —
   safer and unambiguous: give the canvas no special stacking at all
   (plain document order, first child) and wrap every other hero child
   in .ps-hero-inner with a real position:relative + z-index:1, so
   "on top" is asserted positively by the content instead of relying on
   "behind" being asserted negatively by the canvas. */
.ps-hero-fireworks { position: absolute; inset: 0; width: 100%; height: 100%; }
.ps-hero-inner { position: relative; z-index: 1; }
@media (prefers-reduced-motion: reduce) { .ps-hero-fireworks { display: none; } }
.ps-hero-badges { display: flex; gap: .6rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1.4rem; }
.ps-pill {
  display: inline-flex; align-items: center; gap: .4rem;
  background: var(--sky-800); border: 1px solid var(--line); color: var(--ink-dim);
  font-family: var(--head); font-weight: 600; font-size: .82rem; letter-spacing: .05em; text-transform: uppercase;
  padding: .4rem .85rem; border-radius: 999px;
}
.ps-pill.spark { color: var(--sky-950); background: var(--spark); border-color: var(--spark); }
.ps-hero h1 {
  font-size: clamp(2.4rem, 6vw, 4.6rem); line-height: .98; color: var(--ink);
  text-transform: uppercase; letter-spacing: .01em; max-width: 900px; margin: 0 auto;
}
/* Chad iterated this one live: "add the white outline to the blue text
   and blue outlines" [hero + countdown] → "Make the white outline
   thinner, remove it from the open text and dates" → "Remove outline
   from countdown text also" → "the numbers and the boxes should have
   outlines still" → "outline the numbers below the countdown feature
   also" [.ps-stat-num, the stats strip just under the countdown card] →
   "remove white outline from countdown elements except on numbers".
   Landed on: the hero's accent line, the countdown's own big numbers
   (.ps-countdown-num below), and the stats-strip numbers (.ps-stat-num)
   all keep an outline; nothing else in the countdown card does (not the
   label, not the completion message, not the tile boxes, not the card's
   own border) and the "Open Seasonally"/dates lines never had one.
   em-based offset (not a flat px) so the stroke scales with each
   element's own font-size, same idea as the logo's own white-outline
   treatment (public-site.css's .ps-logo-outline / styles.css's
   .logo-outline) just via text-shadow instead of filter: drop-shadow,
   the right tool for real text. */
.ps-hero h1 .accent {
  color: var(--spark);
  text-shadow:
    .02em 0 0 #fff, -.02em 0 0 #fff,
    0 .02em 0 #fff, 0 -.02em 0 #fff,
    .02em .02em 0 #fff, -.02em -.02em 0 #fff,
    .02em -.02em 0 #fff, -.02em .02em 0 #fff;
}
.ps-hero-sub {
  max-width: 620px; margin: 1.2rem auto 0; color: var(--ink-dim); font-size: 1.15rem;
}
/* Chad: 'Where it says open, change it to say "Open Seasonally" and
   then below the dates' — two stacked lines now (see countdownHtml's
   caller in public-site.js) instead of one "Open <dates>" line. Later
   asked to see it in the same neon-flicker treatment as the "Open Past
   Midnight New Year" heading below the countdown (.ps-flicker-heading,
   same ps-neon-flicker keyframes reused here rather than duplicated),
   then specifically: "Open Seasonally" in spark blue, the dates still
   in ember red, so the two lines get their own glow color instead of
   sharing one. */
.ps-hero-dates {
  margin-top: 1.6rem; font-family: var(--head); font-weight: 700;
  text-transform: uppercase; letter-spacing: .1em;
  font-size: clamp(1.1rem, 3vw, 1.5rem);
  animation: ps-neon-flicker 3.2s infinite;
}
.ps-hero-dates div:first-child {
  color: #cfe0f7;
  text-shadow: 0 0 2px #cfe0f7, 0 0 6px var(--spark-bright), 0 0 14px var(--spark-bright), 0 0 28px var(--spark-bright);
}
.ps-hero-dates div:last-child {
  color: #ffd9d4; margin-top: .3rem;
  text-shadow: 0 0 2px #ffd9d4, 0 0 6px var(--ember), 0 0 14px var(--ember), 0 0 28px var(--ember);
}
@media (prefers-reduced-motion: reduce) { .ps-hero-dates { animation: none; } }
.ps-hero-cta { display: flex; gap: 1rem; justify-content: center; margin-top: 2rem; flex-wrap: wrap; }
.ps-btn {
  display: inline-block; text-decoration: none; font-family: var(--head); font-weight: 700;
  letter-spacing: .03em; text-transform: uppercase; padding: .85rem 1.7rem; border-radius: 10px;
  font-size: .98rem; transition: transform .12s, background .15s; border: 1px solid transparent;
}
.ps-btn:hover { transform: translateY(-1px); }
.ps-btn.primary { background: var(--spark); color: var(--sky-950); }
.ps-btn.primary:hover { background: var(--spark-bright); }
.ps-btn.ghost { background: transparent; border-color: var(--line); color: var(--ink); }
.ps-btn.ghost:hover { border-color: var(--ink-dim); }
/* Full-width, for a button stacked inside a narrow card rather than
   sitting inline in a row — the gift-card reveal flow's own buttons
   (sharedGiftCardHtml in public-site.js). */
.ps-btn.block { display: block; width: 100%; text-align: center; }

/* promo strip */
.ps-promo {
  margin: 2.6rem auto 0; max-width: 720px;
  background: linear-gradient(135deg, var(--sky-800), var(--sky-700));
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.3rem 1.6rem; text-align: left; display: flex; align-items: center; gap: 1rem;
}
.ps-promo-mark { font-family: var(--display); font-size: 1.6rem; color: var(--spark); flex-shrink: 0; }
.ps-promo-text { color: var(--ink); font-size: .98rem; }

/* countdown — Chad: "a countdown timer for new years at midnight as the
   reference site did for the 4th of july. The countdown should be
   programmable" — one target datetime + label, styled for whichever
   occasion is currently set rather than baked in for one holiday. */
/* Chad: "lets find a way to make the countdown larger" — a real card
   (gradient surface + gold border, same family as .ps-promo below) so it
   reads as a standout feature instead of a quiet row of numbers, with
   bigger tiles and tile numbers to match. */
.ps-countdown {
  position: relative; overflow: hidden;
  margin: 2.6rem auto 0; max-width: 720px; text-align: center;
  background: linear-gradient(135deg, var(--sky-800), var(--sky-700));
  border: 1px solid var(--spark); border-radius: var(--radius);
  padding: 2rem 1.6rem 1.8rem;
}
/* Chad: "Think we can fit this in the background of the countdown box"
   — a real Cincinnati skyline-and-water illustration (the actual city
   across the river from Northern Kentucky), used as the provided file
   directly rather than redrawn — same lesson as the shelf-label border
   attempt earlier: work from the real asset, don't try to reconstruct
   it from path data.
   z-index:-1 on the skyline layer was the first attempt and, live-tested,
   never actually painted at all — same rendering quirk the hero's
   canvas hit earlier this session, so evidently not unique to <canvas>
   the way that fix assumed. Fixed the same way: no special stacking on
   the skyline layer at all (plain z-index:auto, first child), and the
   real content (.ps-countdown-content / .ps-countdown-message) asserts
   "on top" positively instead, via position:relative + z-index:1.
   Chad: "The skyline svg behind the countdown on mobile does not
   properly render." A plain CSS background here (as this was originally)
   has no way to crop out the source file's own real transparent margin
   below the artwork's ground line without a fixed-pixel position nudge —
   that nudge only held at the one box width it was tuned against, and
   pushed most of the skyline out of view at mobile's much narrower box.
   Rebuilt as a real <img> instead, same technique already proven on the
   mailer cover's own skyline (.mc-cover-skyline in app.js): aspect-ratio
   on the wrapper matches the artwork's actual measured content box
   (2321.57 x 674.229, not the file's full 836.812-tall canvas), with
   object-fit: cover + object-position: top doing a display-time crop —
   scales correctly at any box width, no per-breakpoint tuning, and never
   touches the source file (Chad, of this same asset, elsewhere: "Do not
   change any contents of the image other than size for scaling"). */
.ps-countdown-skyline-wrap {
  position: absolute; left: 0; right: 0; bottom: 0;
  aspect-ratio: 2321.57 / 674.229; overflow: hidden;
}
.ps-countdown-skyline { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; opacity: .4; }
.ps-countdown-content { position: relative; z-index: 1; }
.ps-countdown-label {
  font-family: var(--head); font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--spark-bright); font-size: 1.25rem; margin-bottom: 1.2rem;
}
.ps-countdown-tiles { display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; }
/* Chad: "remove white outline from countdown elements except on
   numbers" — back to the plain var(--line) border, outline stays on
   .ps-countdown-num alone. */
.ps-countdown-tile {
  background: var(--sky-950); border: 1px solid var(--line); border-radius: 12px;
  min-width: 100px; padding: 1.1rem .7rem .8rem;
}
.ps-countdown-num {
  font-family: var(--display); font-size: clamp(2.4rem, 7vw, 3.8rem); color: var(--spark);
  line-height: 1; font-variant-numeric: tabular-nums;
  text-shadow:
    .02em 0 0 #fff, -.02em 0 0 #fff,
    0 .02em 0 #fff, 0 -.02em 0 #fff,
    .02em .02em 0 #fff, -.02em -.02em 0 #fff,
    .02em -.02em 0 #fff, -.02em .02em 0 #fff;
}
.ps-countdown-unit {
  font-family: var(--head); font-size: .8rem; letter-spacing: .07em; text-transform: uppercase;
  color: var(--ink-faint); margin-top: .5rem;
}
.ps-countdown-message {
  position: relative; z-index: 1;
  font-family: var(--display); text-transform: uppercase; letter-spacing: .01em;
  font-size: clamp(1.5rem, 4vw, 2rem); color: var(--spark-bright);
}

/* Chad: "small heading style, flicker, ember red... right under the
   countdown timer... OPEN PAST MIDNIGHT NEW YEAR" — the "flicker, small
   heading" neon study, at the site's own --ember rather than a generic
   red, with an irregular tube-catching stutter rather than a steady
   pulse (off entirely under reduced motion). */
.ps-flicker-heading {
  text-align: center; font-family: var(--head); font-weight: 700;
  text-transform: uppercase; letter-spacing: .1em; margin-top: 1.4rem;
  font-size: clamp(1.1rem, 3vw, 1.5rem); color: #ffd9d4;
  text-shadow: 0 0 2px #ffd9d4, 0 0 6px var(--ember), 0 0 14px var(--ember), 0 0 28px var(--ember);
  animation: ps-neon-flicker 3.2s infinite;
}
@keyframes ps-neon-flicker {
  0%, 3%, 6%, 100% { opacity: 1; }
  4% { opacity: .4; }
  5% { opacity: .85; }
  52% { opacity: 1; }
  53% { opacity: .5; }
  54% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .ps-flicker-heading { animation: none; } }

/* stats strip */
.ps-stats {
  border-top: 1px solid var(--line); border-bottom: 1px solid var(--line);
  background: var(--sky-900);
}
.ps-stats-grid {
  max-width: var(--wrap); margin: 0 auto; padding: 2.2rem 1.5rem;
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 1.5rem; text-align: center;
}
/* Chad: "outline the numbers below the countdown feature also" — the
   stats strip sits directly under the countdown card; same thin,
   em-based white outline as .ps-countdown-num just above it. */
.ps-stat-num {
  font-family: var(--display); font-size: 2.4rem; color: var(--spark); line-height: 1;
  text-shadow:
    .02em 0 0 #fff, -.02em 0 0 #fff,
    0 .02em 0 #fff, 0 -.02em 0 #fff,
    .02em .02em 0 #fff, -.02em -.02em 0 #fff,
    .02em -.02em 0 #fff, -.02em .02em 0 #fff;
}
.ps-stat-label { font-family: var(--head); font-size: .82rem; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint); margin-top: .4rem; }

/* brand strip — Chad: "between the product counter section and the what
   we carry section add a automatic side scrolling banner of brand
   logos. only show brands currently active on the website."
   Chad, after the first version: "unacceptable stutter... logos have
   loading errors and will disappear often [on mobile]... You may need
   to entirely change how the feature is implemented." Rebuilt on two
   changes: every logo is now one plain, pre-baked <img> (no runtime
   filter: drop-shadow, no stacked/overlaid image pairs — see BRAND_LOGOS
   in public-site.js), and the repeat count + animation speed are set
   once by wireBrandStrip() from the real measured width of one copy,
   not guessed or hardcoded — see that function's own comment for why a
   flat repeat count or a flat duration both broke down as the on-screen
   width changed. width: max-content keeps the track from being squeezed
   to the viewport, which the translateX(-50%) loop math depends on.
   .ps-brand-ready (added by wireBrandStrip once it's measured and set a
   real animation-duration) is what actually turns the animation on —
   without it the page would briefly show one un-repeated, untimed copy
   before JS finishes. */
.ps-brand-strip { overflow: hidden; padding: 1.7rem 0; border-bottom: 1px solid var(--line); }
.ps-brand-track { display: flex; align-items: center; width: max-content; }
.ps-brand-track.ps-brand-ready {
  animation-name: ps-scroll-brands; animation-timing-function: linear; animation-iteration-count: infinite;
  /* Hints the browser to keep this on its own GPU layer for the whole
     loop, not just while actively animating — without it, some browsers
     drop and re-promote the compositing layer right at the instant an
     infinite animation restarts (0% after 100%), which can show up as a
     single-frame flash/flicker exactly at the loop point. */
  will-change: transform;
}
.ps-brand-strip:hover .ps-brand-track { animation-play-state: paused; }
@keyframes ps-scroll-brands {
  from { transform: translateX(0); }
  to { transform: translateX(-50%); }
}
/* Chad: "it appears to have a visual glitch when it reaches the end of
   the loop" — flex `gap` was the original cause: with N logos duplicated
   to 2N, a uniform gap gives 2N-1 gaps total, and -50% of that (an odd
   count) lands half a gap short of where the second copy actually
   starts. Spacing lives on each logo's own margin-right instead
   (including the last logo of each copy) — every item then carries its
   own trailing space with nothing "shared" at the seam, so exactly half
   the track's total width really is one full, identical copy. */
/* Chad: "put white text under the harder to read ones," then "let's
   make it all match" — every logo is an image plus a real text caption
   underneath now, stacked in a column (the row layout lives one level
   up, on .ps-brand-track). */
/* Chad: "when clicking on a brand in the scrolling feature I'd like it
   to take them to the product page with that brand filtered as
   showing" — .ps-brand-logo is a real <a> now, not a plain <div>; reset
   the anchor defaults and add a small hover cue so it actually reads as
   clickable, same transform-on-hover convention .ps-cat-card already uses. */
.ps-brand-logo {
  display: flex; flex-direction: column; align-items: center; gap: .35rem; flex-shrink: 0; margin-right: 3.5rem;
  text-decoration: none; color: inherit; transition: transform .15s ease;
}
.ps-brand-logo:hover { transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { .ps-brand-logo { transition: none; } .ps-brand-logo:hover { transform: none; } }
.ps-brand-logo img { height: 52px; width: auto; display: block; }
.ps-brand-label { color: var(--ink); font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }

/* ---------------- generic section chrome ---------------- */
.ps-section { padding: 4rem 1.5rem; }
.ps-section.alt { background: var(--sky-900); }
.ps-eyebrow {
  font-family: var(--head); font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--spark); font-size: .85rem; margin-bottom: .5rem; display: block;
}
.ps-section-head { max-width: var(--wrap); margin: 0 auto 2.2rem; text-align: center; }
.ps-section-head h2 { font-size: clamp(1.7rem, 3.5vw, 2.4rem); text-transform: uppercase; }
.ps-section-head p { color: var(--ink-dim); margin-top: .6rem; max-width: 560px; margin-inline: auto; }

/* category cards. flex-wrap + justify-content:center, not a grid — with
   a real catalog's worth of categories this wraps into several rows, and
   a grid leaves whatever's left in the last row flush left instead of
   centered under the full rows above it (Chad: "add the same centering
   logic to the 'what we carry' section," after the same fix on the perks
   section below — see .ps-values.wrap-center's own comment for why a
   grid can't do this on its own). max-width keeps a short last row's
   cards from stretching wider than the full rows above just because
   there's leftover space to hand out. */
.ps-cats { max-width: var(--wrap); margin: 0 auto; display: flex; flex-wrap: wrap; justify-content: center; gap: 1.1rem; }
.ps-cat-card {
  flex: 1 1 200px; max-width: 260px;
  background: var(--sky-800); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.6rem 1.3rem; text-decoration: none; transition: border-color .15s, transform .15s;
}
.ps-cat-card:hover { border-color: var(--spark); transform: translateY(-2px); }
.ps-cat-icon { font-size: 1.8rem; margin-bottom: .6rem; }
.ps-cat-name { font-family: var(--head); font-weight: 700; text-transform: uppercase; letter-spacing: .02em; color: var(--ink); font-size: 1.05rem; }
.ps-cat-count { color: var(--ink-faint); font-size: .88rem; margin-top: .2rem; }
/* Chad: "I like the photo tile ... build for real" — a category with a
   real uploaded photo (category-images.js, set from the public-site
   Products screen) gets a proper frame instead of the plain 1.8rem emoji
   slot above; mocked up first against his own product renders before
   this was wired to real dashboard-managed data. */
.ps-cat-card.photo { padding: 0 0 1.2rem; overflow: hidden; }
.ps-cat-photo-frame {
  background: var(--sky-700); height: 132px; display: flex; align-items: center; justify-content: center;
  margin-bottom: 1rem; padding: .6rem;
}
.ps-cat-photo-frame img { max-width: 100%; max-height: 100%; object-fit: contain; }
.ps-cat-card.photo .ps-cat-name, .ps-cat-card.photo .ps-cat-count { padding: 0 1.2rem; }
@media (max-width: 780px) {
  /* .ps-cat-card ends up one full-width column on a real phone screen
     (flex-wrap only fits one 200px-basis card per row once the viewport
     itself is that narrow) — the icon/name/count were still left-aligned
     from the desktop multi-column layout, which reads as off-center once
     the card is the full width of the screen instead of a narrow tile.
     Chad: "the category icon is not centered on mobile." */
  .ps-cat-card { text-align: center; }
}
@media (max-width: 560px) {
  /* Chad: "the cards for what we carry and more ways to save... currently
     go to single wide [on mobile], let's make them stack in 2 columns."
     A real phone-only breakpoint (matches .ps-account-tabs' own 560px cutoff
     below), not the 780px tablet one above — a tablet still has the room
     for the flex-wrap card widths to fall into 3+ per row on their own. */
  .ps-cats { display: grid; grid-template-columns: repeat(2, 1fr); gap: .8rem; }
  /* Chad: "center the lone card if needed" — an odd category count
     leaves one card by itself in the last row, flush left under column
     one. :last-child:nth-child(odd) only matches when the last card
     also sits at an odd position, i.e. exactly when the total count is
     odd — an even count (no orphan) never matches this and is untouched.
     width matches what a normal column would be (half the row minus half
     the gap) so the lone card reads as the same size as the rows above
     it, just centered across both columns instead of stretched full width. */
  .ps-cats > .ps-cat-card:last-child:nth-child(odd) { grid-column: 1 / -1; justify-self: center; width: calc(50% - .4rem); }
}

/* value props */
.ps-values { max-width: var(--wrap); margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1.3rem; }
.ps-value { text-align: center; padding: 1rem; }
.ps-value-icon { font-size: 2rem; margin-bottom: .7rem; }
.ps-value h3 { font-size: 1.05rem; text-transform: uppercase; letter-spacing: .02em; margin-bottom: .4rem; }
.ps-value p { color: var(--ink-dim); font-size: .94rem; }
/* The home page's "More Ways To Save" perks (wishlist/points/referrals/
   status/live-selection) reuse .ps-value's exact layout, but each one is
   a real link to its own page — Chad: "explainers why to shop at our
   stand including the new features" — so unlike the plain .ps-value
   trust cards above them, these need to read as clickable. */
.ps-value-link {
  display: block; text-decoration: none; color: inherit; border-radius: var(--radius);
  background: var(--sky-800); border: 1px solid var(--line); transition: border-color .2s, transform .2s;
}
.ps-value-link:hover { border-color: var(--spark-bright); transform: translateY(-2px); }
.ps-value-link h3 { color: var(--ink); }
/* .ps-values is a plain grid everywhere else (always exactly 3 items —
   "Why Bluegrass" fills its row completely, so it never hits this), but
   the perks section above can render 3 OR 5 cards depending on which
   programs are enabled — 5 in a 3-column grid leaves a 2-card last row
   sitting flush left under the row of 3. Chad: "the bottom 2 should be
   centered on the group of 3." A CSS grid can't center a short last row
   on its own; flex-wrap can, since justify-content only ever looks at
   how much space a wrapped line actually used. Scoped to .wrap-center
   rather than changed on .ps-values itself, so "Why Bluegrass" keeps its
   plain grid untouched. max-width caps how wide a card grows when
   there's extra room to hand out (a lone centered pair would otherwise
   stretch noticeably wider than the row of 3 above it). */
.ps-values.wrap-center { display: flex; flex-wrap: wrap; justify-content: center; }
.ps-values.wrap-center > .ps-value-link { flex: 1 1 300px; max-width: 380px; }
/* Chad: "the cards for what we carry and more ways to save... currently
   go to single wide [on mobile], let's make them stack in 2 columns" —
   same real phone-only breakpoint as .ps-cats' own fix above; switches to
   a plain 2-column grid here too (wrap-center's own flex-wrap centering
   trick only matters for the 3-wide desktop grid it was built for). */
@media (max-width: 560px) {
  .ps-values.wrap-center { display: grid; grid-template-columns: repeat(2, 1fr); gap: .8rem; }
  .ps-values.wrap-center > .ps-value-link { max-width: none; }
  /* Chad: "center the lone card if needed" — same odd-count-orphan fix
     as .ps-cats' own, just against .ps-value-link (perks can render 3 or
     5 cards; either count is odd, so this always applies down here). */
  .ps-values.wrap-center > .ps-value-link:last-child:nth-child(odd) { grid-column: 1 / -1; justify-self: center; width: calc(50% - .4rem); }
}

/* ---------------- product grid ---------------- */
.ps-grid { max-width: var(--wrap); margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 1.3rem; }
.ps-card {
  background: var(--sky-800); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden;
  display: flex; flex-direction: column; position: relative;
}
.ps-card.featured { border-color: var(--spark); box-shadow: 0 0 0 1px var(--spark) inset; }
.ps-card-ribbon {
  position: absolute; top: .7rem; left: .7rem; background: var(--spark); color: var(--sky-950);
  font-family: var(--head); font-weight: 700; font-size: .72rem; letter-spacing: .05em; text-transform: uppercase;
  padding: .25rem .6rem; border-radius: 999px; z-index: 1;
}
.ps-card-photo { position: relative; aspect-ratio: 4/3; background: var(--sky-700); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.ps-card-photo img { width: 100%; height: 100%; object-fit: contain; }
.ps-card-noPhoto { font-size: 2.4rem; opacity: .5; }
/* Chad: "lets make videos play on the product page in an in page viewer
   just like on the mailer page." Same "Play video" pill placement/shape
   as the mailer webpage's own .shop-card-play (public/shop.css) — a
   small triangle + label pinned to the photo's corner, not the emoji
   button an earlier round of feedback already moved the mailer page
   away from for the same reason. */
.ps-card-play {
  position: absolute; right: .6rem; bottom: .6rem; background: rgba(7,10,22,.88); color: var(--ink);
  border: none; border-radius: 999px; padding: .35rem .8rem; display: flex; align-items: center;
  gap: .35rem; font-family: var(--head); font-weight: 700; font-size: .78rem; letter-spacing: .02em;
  text-transform: uppercase; line-height: 1; box-shadow: 0 2px 8px rgba(0,0,0,.35); white-space: nowrap;
  cursor: pointer;
}
.ps-card-play::before {
  content: ''; width: 0; height: 0; border-style: solid; border-width: 4px 0 4px 7px;
  border-color: transparent transparent transparent var(--spark-bright); flex: none;
}
/* Save-for-later, opposite corner from the ribbon — a plain heart glyph
   rather than an icon font, same "no new asset dependency" reasoning
   .ps-card-play's own triangle already follows. */
.ps-card-wish {
  position: absolute; top: .6rem; right: .6rem; width: 32px; height: 32px; border-radius: 50%;
  background: rgba(7,10,22,.72); border: none; color: #fff; font-size: 1.15rem; line-height: 1;
  display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 1;
  transition: transform .12s ease, background .15s ease;
}
.ps-card-wish:hover { transform: scale(1.08); }
.ps-card-wish.active { background: var(--spark); color: #fff; }
.ps-card-body { padding: 1rem 1.1rem 1.2rem; display: flex; flex-direction: column; gap: .4rem; flex: 1; }
.ps-card-cat { font-family: var(--head); font-size: .74rem; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-faint); }
.ps-card-name { font-weight: 600; font-size: 1.02rem; color: var(--ink); }
.ps-card-price { font-family: var(--head); font-weight: 700; color: var(--spark-bright); font-size: 1.05rem; }
.ps-card-blurb { color: var(--ink-dim); font-size: .88rem; margin-top: .1rem; }

.ps-cat-group { max-width: var(--wrap); margin: 0 auto 3rem; }
.ps-cat-group h2 { font-size: 1.4rem; text-transform: uppercase; letter-spacing: .02em; margin-bottom: 1.1rem; padding-bottom: .6rem; border-bottom: 1px solid var(--line); }

/* Chad: "Lets add category and sort functions to the products page." The
   category pills reuse .ps-pill (the same badge already used for the
   hero's "Now Stocking" / "Family Owned" chips) as real <button>s instead
   of static spans — this rule just strips the button-element defaults
   .ps-pill itself doesn't need to know about. */
.ps-products-controls {
  max-width: var(--wrap); margin: 0 auto 2rem; display: flex; align-items: center;
  justify-content: space-between; flex-wrap: wrap; gap: 1rem;
}
.ps-cat-pills { display: flex; flex-wrap: wrap; gap: .6rem; }
.ps-cat-pill { font: inherit; cursor: pointer; }
.ps-cat-pill:hover { border-color: var(--spark); }
/* Chad: "Brand selector should not be a pill selector like category,
   make it a drop down list" — grouped with the sort dropdown (not the
   category pills) so the controls row reads as "pills on the left,
   dropdowns on the right" rather than three independent blocks spread
   out by justify-content: space-between. */
.ps-select-group { display: flex; flex-wrap: wrap; gap: .6rem; }
.ps-sort-select {
  background: var(--sky-800); border: 1px solid var(--line); color: var(--ink);
  font-family: var(--head); font-weight: 600; font-size: .85rem; letter-spacing: .02em;
  padding: .55rem .9rem; border-radius: 8px; cursor: pointer;
}
@media (max-width: 780px) {
  /* Pills can wrap several rows on a narrow phone — give the dropdowns
     their own full-width rows underneath rather than fighting the pills
     for the remaining space on the same line. */
  .ps-products-controls { flex-direction: column; align-items: stretch; }
  .ps-select-group { flex-direction: column; }
  .ps-sort-select { width: 100%; }
}

/* ---------------- locations ---------------- */
.ps-locations { max-width: var(--wrap); margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }
.ps-loc-card { background: var(--sky-800); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.6rem; }
.ps-loc-card h3 { font-size: 1.25rem; text-transform: uppercase; margin-bottom: .6rem; }
.ps-loc-card .addr, .ps-loc-card .phone { color: var(--ink-dim); font-size: .95rem; margin-bottom: .3rem; }
.ps-loc-hours { margin: .9rem 0; border-top: 1px solid var(--line); padding-top: .8rem; }
.ps-loc-hours div { display: flex; justify-content: space-between; font-size: .9rem; color: var(--ink-dim); padding: .15rem 0; }
.ps-loc-hours span:first-child { color: var(--ink-faint); font-family: var(--head); letter-spacing: .03em; }
/* Now usually a live map (a MapLibre GL map rendered into this div, not
   the old static <img> screenshot — see
   locationMapHtml()/wireLocationMaps() in public-site.js), which needs
   its own explicit box the way the original <img> got via this file's
   global img{max-width:100%} reset — a plain div has no intrinsic size
   to fall back on any more than an iframe did. Chad: "the map is too
   small" — was 220px, sized to roughly match the old static
   screenshot's own proportions; a live embed is worth more room to
   actually pan/zoom in.
   History: this was a Google Maps `?output=embed` iframe (no API key
   needed), then dark-moded via a CSS invert()+hue-rotate() filter (Chad:
   "Can the google map used be dark mode?") since Google's free embed has
   no real custom-style option of its own — a genuine dark theme there
   needed an API key, a Cloud billing project, and the full JS Maps
   library. Then Chad: "Can we use apple maps instead of google maps,"
   and once told a real Apple Maps embed needs a paid Apple Developer
   account + MapKit JS token, asked for "a different dependency free map
   that looks better and can be customized" instead — first tried
   Leaflet + CARTO's "dark_all" tile set, which turned out (checked live,
   not just assumed) to now require a paid key; landed on MapLibre GL +
   OpenFreeMap's hosted "dark" style instead, a REAL dark theme (no
   filter trick needed; background-color here is just what shows for the
   instant before MapLibre's own tiles paint in) from a service built
   specifically to stay free with no signup. */
.ps-loc-map {
  display: block; width: 100%; height: 360px; border: 0; border-radius: 10px; margin: .9rem 0; overflow: hidden;
  background: var(--sky-800);
}
/* MapLibre's own zoom control and attribution strip are styled for
   OpenFreeMap's dark style by default — legible without any override.
   The marker is a plain div (wireLocationMaps() in public-site.js hands
   this class straight to maplibregl.Marker), a simple dot rather than a
   drawn pin shape, colored via --spark-bright rather than a hardcoded
   hex so it stays the site's actual current accent color if that's ever
   changed again. */
.ps-loc-pin {
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--spark-bright); border: 3px solid var(--sky-950);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .5);
}
.ps-loc-card .ps-btn { margin-top: .4rem; }
/* Chad: "on the locations center the text and make the headings/text
   bigger," then "the opening days and hours can stay how they were" —
   scoped to .ps-locations (the Locations page's own card grid) rather
   than the bare .ps-loc-card rules above, which the Contact page also
   reuses for a smaller, plain name+phone card that wasn't part of this
   request. Name switches to the bigger, bolder display face already
   used for other card headings on this site (.ps-info-card h3,
   .ps-check-item h3). The hours rows (.ps-loc-hours) are deliberately
   NOT touched here — still the original space-between/.9rem layout from
   the base rules above. */
.ps-locations .ps-loc-card { text-align: center; }
.ps-locations .ps-loc-card h3 { font-family: var(--display); font-size: 1.7rem; }
.ps-locations .ps-loc-card .addr, .ps-locations .ps-loc-card .phone { font-size: 1.1rem; }

/* Chad bought a vanity number ("859-800-BOOM") and wants it shown large —
   phoneDisplayHtml() in public-site.js only adds the .vanity modifier
   when the stored phone string actually has letters in it, so a normal
   digits-only number (most locations, and the Contact page's own plain
   cards) is unaffected. .ps-loc-card .phone.vanity (not a bare
   .phone.vanity) matches the specificity of the Locations-page-only
   font-size rule just above so this reliably overrides it there too,
   not just on Contact's plainer cards. .phone-dial is the "someone that
   doesn't know" fallback (Chad's own words) — the actual digits to
   dial, small and muted underneath rather than assumed obvious. */
.ps-loc-card .phone.vanity {
  font-family: var(--display); font-size: clamp(1.6rem, 4.5vw, 2.3rem); line-height: 1.15;
  color: var(--caution-bright); letter-spacing: .02em; margin: .5rem 0 0;
}
.phone-dial { display: block; font-family: var(--body); font-size: .78rem; font-weight: 400; letter-spacing: .03em; color: var(--ink-faint); margin: .3rem 0 .3rem; }

/* ---------------- plain copy pages (safety/about/contact) ---------------- */
.ps-prose { max-width: 720px; margin: 0 auto; color: var(--ink-dim); font-size: 1.05rem; }
.ps-prose h2 { color: var(--ink); font-size: 1.6rem; text-transform: uppercase; margin-bottom: 1rem; }
.ps-prose p { margin-bottom: 1rem; white-space: pre-wrap; }
.ps-contact-cards { max-width: 720px; margin: 2rem auto 0; display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.ps-contact-cards .ps-loc-card { padding: 1.2rem 1.4rem; }
/* Chad: "in the bottom box also center the name, address and phone
   number, add open hours and dates there too" — same "center everything
   except the hours rows" split the Locations page's own cards already
   use (see that rule's comment above for why .ps-loc-hours stays
   space-between on purpose). */
.ps-contact-cards .ps-loc-card { text-align: center; }
.ps-loc-dates { color: var(--ink-dim); font-size: .85rem; margin-top: .3rem; }

/* Chad: "Add functionality to the contact us page, add a form to fill." */
.ps-contact-form-wrap {
  max-width: 560px; margin: 0 auto 2.5rem; background: var(--sky-900); border: 1px solid var(--line); border-radius: var(--radius); padding: 2rem;
  /* Chad: "center the thanks for reaching out text" — the success state
     (h3 + p, swapped in over the form on submit — see wireContactForm)
     is plain text with nothing to type into, so it centers by default;
     the form itself opts back to left below, same split the newsletter
     popup's own box/form pair already uses. */
  text-align: center;
}
.ps-contact-form-wrap h3 { font-family: var(--head); font-size: 1.3rem; margin: 0 0 1.2rem; }
.ps-contact-form-wrap > p { color: var(--ink-dim); margin: 0; }
#psContactForm { display: flex; flex-direction: column; gap: .8rem; text-align: left; }
.ps-contact-row { display: flex; gap: .8rem; flex-wrap: wrap; }
.ps-contact-row input { flex: 1; min-width: 180px; }
#psContactForm input[type="text"], #psContactForm input[type="email"], #psContactForm input[type="tel"], #psContactForm textarea {
  background: var(--sky-950); border: 1px solid var(--line); border-radius: 8px; color: var(--ink);
  padding: .7rem .9rem; font: inherit; width: 100%; resize: vertical;
}
#psContactForm input::placeholder, #psContactForm textarea::placeholder { color: var(--ink-faint); }
#psContactForm button[type="submit"] { align-self: center; }

/* ---------------- safety page ---------------- */
/* A bolder, more graphical heading treatment than the plain
   .ps-section-head other pages use — bigger, heavier, with a short
   accent underline — since this page carries more visual sections back
   to back and needs stronger dividers between them. */
.ps-safety-head { text-align: center; margin: 0 auto 2rem; }
.ps-safety-head h2 {
  font-family: var(--display); font-size: clamp(2.1rem, 5vw, 3.2rem); line-height: 1;
  text-transform: uppercase; letter-spacing: .01em; text-wrap: balance;
}
.ps-safety-head h2::after {
  content: ''; display: block; width: 64px; height: 4px; background: var(--spark);
  margin: .9rem auto 0; border-radius: 2px;
}
/* .ps-safety-head normally trusts the *previous* element's own bottom
   margin for spacing (that's how the two mid-page uses on the real
   Safety page work — .ps-prose/.ps-info-grid both end in margin-bottom).
   /rewards stacks several of these back to back after elements that
   don't reliably carry trailing space of their own (.ps-pitch-hero has
   none at all — its one real page relies on .ps-pitch-steps' own
   top margin instead — and .ps-refflat ends flush too, no bottom
   margin of its own), so those two ended up jammed together with no gap between
   them at all. A dedicated modifier, only applied on /rewards, rather
   than changing .ps-safety-head's own default and risking the Safety
   page's already-correct spacing. */
.ps-safety-head.ps-rewards-gap { margin-top: 3.4rem; }
/* .ps-safety-head's own h2 size (clamp(2.1rem, 5vw, 3.2rem)) was tuned
   for the Safety page, where it opens the page with nothing bigger
   above it. On /rewards it sits *under* .ps-pitch-h1's real hero, so at
   the Safety page's own size the two were nearly the same height
   (3.2rem vs 3.4rem) — the hero read as barely bigger than its own
   subsections, not clearly the biggest thing on the page. Scaled down
   so the hierarchy is unambiguous: hero, then section, then body copy. */
.ps-safety-head.ps-rewards-gap h2 { font-size: clamp(1.6rem, 3.2vw, 2.2rem); }

/* info grid — Chad: "make the info grid titles larger and more
   graphical," then, on the boxed-icon-stacked-above-the-title version:
   "make the emojis in line with the text, it looks odd." Icon sits
   beside the heading on the same row instead — a plain bigger emoji, no
   separate icon box. */
.ps-info-grid { max-width: var(--wrap); margin: 0 auto 3rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.1rem; }
.ps-info-card { display: flex; align-items: flex-start; gap: 1.1rem; background: var(--sky-800); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.4rem 1.3rem; }
/* flex-start, not center: these cards run a lot taller than a short
   heading-plus-paragraph would need, so centering against the whole
   block pulled the icon down toward the paragraph instead of sitting
   with the heading it belongs to. A small top nudge lines its visual
   center up with the heading's cap-height. */
.ps-info-icon { font-size: 2rem; line-height: 1; flex-shrink: 0; margin-top: .1rem; }
.ps-info-card h3 { font-family: var(--display); font-size: 1.15rem; text-transform: uppercase; letter-spacing: .01em; color: var(--ink); margin-bottom: .4rem; line-height: 1.05; }
.ps-info-card p { color: var(--ink-dim); font-size: .92rem; }

/* six-habit checklist — Chad: "on wide screen formats the 5 and 6 align
   left, lets have those align center." Fixed column counts (2 then 3),
   not auto-fit off a minmax, so six items always fill complete rows —
   an orphaned last row is what was pulling 5/6 to the left in the first
   place. */
.ps-checklist { max-width: var(--wrap); margin: 0 auto 3rem; display: grid; grid-template-columns: 1fr; gap: 1.4rem; }
@media (min-width: 620px) { .ps-checklist { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 960px) { .ps-checklist { grid-template-columns: repeat(3, 1fr); } }
.ps-check-item { display: flex; gap: 1rem; }
.ps-check-num {
  flex-shrink: 0; width: 2.8rem; height: 2.8rem; border-radius: 8px; background: var(--sky-800); border: 1px solid var(--spark);
  color: var(--spark); font-family: var(--display); font-size: 1.3rem; display: flex; align-items: center; justify-content: center;
}
.ps-check-item h3 { font-family: var(--display); font-size: 1.15rem; text-transform: uppercase; letter-spacing: .01em; margin-bottom: .3rem; color: var(--ink); }
.ps-check-item p { color: var(--ink-dim); font-size: .92rem; }

.ps-law-grid { max-width: var(--wrap); margin: 0 auto 2rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.2rem; }
.ps-law-card { background: var(--sky-800); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.4rem 1.5rem; }
.ps-law-card h3 { font-family: var(--display); font-size: 1.3rem; text-transform: uppercase; margin-bottom: .6rem; color: var(--ink); }
.ps-law-card p { color: var(--ink-dim); font-size: .92rem; margin-bottom: .7rem; }
.ps-law-card p:last-child { margin-bottom: 0; }
.ps-law-card a { color: var(--spark-bright); }
.ps-law-card .example { border-top: 1px solid var(--line); margin-top: .8rem; padding-top: .8rem; font-size: .87rem; }

/* Chad: "move that [18+] section to the bottom by the warning and match
   the warning style" — the 18+ requirement used to be its own lighter,
   on-brand .ps-safety-highlight box up near the intro; now it's a second
   .ps-callout stacked with the legal-disclaimer one it always belonged
   next to, same red warning treatment for both. */
.ps-callouts { max-width: var(--wrap); margin: 0 auto; display: flex; flex-direction: column; gap: 1.1rem; }
.ps-callout {
  background: rgba(224,82,74,.12); border: 1px solid var(--ember);
  border-radius: var(--radius); padding: 1.3rem 1.6rem; display: flex; gap: 1rem; align-items: flex-start;
}
.ps-callout-icon { font-size: 1.4rem; flex-shrink: 0; }
.ps-callout h3 { font-size: 1rem; text-transform: uppercase; letter-spacing: .02em; color: var(--ember-bright); margin-bottom: .4rem; }
.ps-callout p { color: var(--ink-dim); font-size: .92rem; }

/* ---------------- footer ---------------- */
.ps-footer { border-top: 1px solid var(--line); padding: 2.5rem 1.5rem; background: var(--sky-950); }
.ps-footer-inner {
  max-width: var(--wrap); margin: 0 auto; display: flex; justify-content: space-between;
  align-items: center; flex-wrap: wrap; gap: 1rem; color: var(--ink-faint); font-size: .88rem;
}
.ps-footer-links { display: flex; gap: 1.2rem; flex-wrap: wrap; }
.ps-footer-links a { text-decoration: none; color: var(--ink-dim); }
.ps-footer-links a:hover { color: var(--spark-bright); }
@media (max-width: 780px) {
  /* Chad: "the footer text is not centered on mobile" — .ps-footer-inner
     is a space-between flex row (copyright left, links right) that's
     fine at desktop widths, but on a narrow phone it wraps to two full-
     width rows and each one keeps its own left-aligned text. */
  .ps-footer-inner { justify-content: center; text-align: center; }
  .ps-footer-links { justify-content: center; }
}

/* Chad: "add a section to the bottom of the main public site to join a
   mailing list... this area should offer a vague discount and offers of
   discounts but no specific offer." Sits right above the plain
   copyright footer, on every page — a lighter band (--sky-900) so it
   reads as its own real section, not part of the fine-print bar below it. */
.ps-footer-newsletter { background: var(--sky-900); border-top: 1px solid var(--line); padding: 3rem 1.5rem; }
.ps-footer-newsletter-inner { max-width: 480px; margin: 0 auto; text-align: center; }
.ps-footer-newsletter-inner h2 { font-family: var(--head); font-size: 1.4rem; margin: 0 0 .5rem; text-wrap: balance; }
.ps-footer-newsletter-inner > p { color: var(--ink-dim); margin: 0 0 1.4rem; }
#psFooterNewsletterForm { display: flex; flex-direction: column; gap: .8rem; text-align: left; }
#psFooterNewsletterForm input[type="text"], #psFooterNewsletterForm input[type="email"] {
  background: var(--sky-950); border: 1px solid var(--line); border-radius: 8px; color: var(--ink);
  padding: .7rem .9rem; font: inherit; width: 100%;
}
#psFooterNewsletterForm input::placeholder { color: var(--ink-faint); }
#psFooterNewsletterForm button[type="submit"] { text-align: center; }

/* ---------------- password gate ---------------- */
.ps-gate {
  min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 2rem;
  background:
    radial-gradient(50% 50% at 50% 20%, rgba(246,185,59,.14), transparent 70%),
    var(--sky-950);
}
.ps-gate-card {
  width: 100%; max-width: 380px; text-align: center;
  background: var(--sky-900); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 2.4rem 2rem;
}
.ps-gate-card img { height: 46px; margin: 0 auto 1rem; }
.ps-gate-card h1 { font-size: 1.5rem; text-transform: uppercase; margin-bottom: .5rem; }
.ps-gate-card p { color: var(--ink-dim); font-size: .92rem; margin-bottom: 1.4rem; }
.ps-gate-form { display: flex; flex-direction: column; gap: .8rem; }
.ps-gate-form input {
  background: var(--sky-800); border: 1px solid var(--line); color: var(--ink);
  border-radius: 8px; padding: .8rem 1rem; font-size: 1rem; text-align: center; letter-spacing: .05em;
}
.ps-gate-form input:focus { outline: 2px solid var(--spark); outline-offset: 1px; }
.ps-gate-error { color: var(--ember-bright); font-size: .88rem; min-height: 1.2em; }
.ps-gate-form button {
  background: var(--spark); color: var(--sky-950); border: none; border-radius: 8px;
  padding: .85rem; font-family: var(--head); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  cursor: pointer; transition: background .15s;
}
.ps-gate-form button:hover { background: var(--spark-bright); }
.ps-gate-form button:disabled { opacity: .6; cursor: default; }

/* ---------------- video modal — same shape as the mailer webpage's own
   (public/shop.css) — an embedded player, never a new tab. ---------- */
.ps-video-modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.ps-video-modal[hidden] { display: none; }
.ps-video-modal-backdrop { position: absolute; inset: 0; background: rgba(7,10,22,.88); }
.ps-video-modal-box { position: relative; z-index: 1; width: 100%; max-width: 780px; }
.ps-video-modal-frame { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 10px; overflow: hidden; }
.ps-video-modal-frame iframe, .ps-video-modal-frame video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.ps-video-modal-close {
  position: absolute; top: -2.6rem; right: 0; background: transparent; border: none; color: var(--ink);
  font-size: 1.4rem; padding: .4rem .6rem; cursor: pointer;
}

/* ---------------- newsletter popup ---------------- */
/* Chad: "Lets add a newsletter signup/popup... it can be a coupon code or
   a free item." Same modal shape/tokens as .ps-video-modal above (fixed
   overlay, dark backdrop, centered box) but its own real estate — a form,
   not a video frame. */
.ps-newsletter-modal { position: fixed; inset: 0; z-index: 110; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.ps-newsletter-modal[hidden] { display: none; }
.ps-newsletter-backdrop { position: absolute; inset: 0; background: rgba(7,10,22,.88); }
.ps-newsletter-box {
  position: relative; z-index: 1; width: 100%; max-width: 440px; background: var(--sky-900);
  border: 1px solid var(--line); border-radius: var(--radius); padding: 2.2rem 2rem;
  animation: ps-newsletter-in .25s ease-out;
  /* Chad: "center align elements for visual pleasure" — the heading,
     subtext, reward and fine print all read as a centered pitch; form
     inputs and the consent line opt back to left below, since centered
     input text jumps around distractingly as someone actually types. */
  text-align: center;
}
@media (prefers-reduced-motion: reduce) { .ps-newsletter-box { animation: none; } }
@keyframes ps-newsletter-in { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }
.ps-newsletter-close {
  position: absolute; top: .6rem; right: .6rem; width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center;
  background: transparent; border: none; color: var(--ink-dim); font-size: 1.1rem; cursor: pointer; border-radius: 50%;
}
.ps-newsletter-close:hover { color: var(--ink); background: var(--sky-800); }
/* Same real wordmark + contrast treatment as the header (see
   newsletterLogoHtml) — Chad: "use the real wordmark," not the
   never-approved horse-mark icon this popup almost got instead. */
.ps-newsletter-logo { display: block; height: 34px; width: auto; margin: 0 auto 1.1rem; }
.ps-newsletter-box h2 { font-family: var(--head); font-size: 1.6rem; margin: 0 0 .5rem; text-wrap: balance; }
.ps-newsletter-box > div > p { color: var(--ink-dim); margin: 0 0 1.2rem; }
#psNewsletterForm { display: flex; flex-direction: column; gap: .7rem; text-align: left; }
#psNewsletterForm input[type="text"], #psNewsletterForm input[type="email"] {
  background: var(--sky-950); border: 1px solid var(--line); border-radius: 8px; color: var(--ink);
  padding: .7rem .9rem; font: inherit; width: 100%; text-align: left;
}
#psNewsletterForm input::placeholder { color: var(--ink-faint); }
#psNewsletterForm button[type="submit"] { text-align: center; }
/* Honeypot — a field named plausibly enough for a bot's autofill to catch,
   invisible and unreachable for a real visitor (off-screen, not just
   opacity:0, and skipped by keyboard/screen-reader nav too). A submission
   with this filled in is silently dropped, never stored — see the
   "company" field check in wireNewsletterForm(). */
.ps-hp { position: absolute !important; left: -9999px !important; width: 1px; height: 1px; opacity: 0; }
.ps-newsletter-consent { display: flex; align-items: flex-start; gap: .6rem; font-size: .85rem; color: var(--ink-dim); cursor: pointer; }
.ps-newsletter-consent input { margin-top: .2rem; flex-shrink: 0; }
.ps-newsletter-fine { font-size: .78rem; color: var(--ink-faint); margin: .2rem 0 0; text-align: center; }
.ps-newsletter-fine a { color: var(--ink-dim); }
.ps-newsletter-error { color: var(--ember-bright); font-size: .85rem; margin: 0; text-align: center; }
.ps-newsletter-reward {
  font-family: var(--head); font-size: 1.3rem; text-align: center; background: var(--sky-950);
  border: 1px dashed var(--spark-bright); border-radius: 8px; padding: 1rem; margin-bottom: 1rem; color: var(--ink);
}
.ps-newsletter-reward.ps-newsletter-code { letter-spacing: .08em; font-size: 1.5rem; color: var(--spark-bright); }

/* ---------------- refer-a-friend (/refer) + account (/account) ---------------- */
#psReferSection, #psAccountSection { text-align: center; }
#psReferSection .ps-contact-form-wrap, #psAccountSection .ps-contact-form-wrap { text-align: center; }
#psReferFriendForm,
#psAccountCreateForm, #psAccountLoginForm, #psAccountForgotForm, #psAccountSetPasswordForm,
#psAccountPreferencesForm, #psPointsClaimForm { display: flex; flex-direction: column; gap: .8rem; text-align: left; }
#psReferFriendForm input[type="text"], #psReferFriendForm input[type="email"], #psReferFriendForm input[type="tel"],
#psAccountCreateForm input[type="text"], #psAccountCreateForm input[type="email"], #psAccountCreateForm input[type="tel"], #psAccountCreateForm input[type="password"],
#psAccountLoginForm input[type="email"], #psAccountLoginForm input[type="password"],
#psAccountForgotForm input[type="email"],
#psAccountSetPasswordForm input[type="password"],
#psPointsClaimForm input[type="date"], #psPointsClaimForm input[type="time"], #psPointsClaimForm input[type="number"] {
  background: var(--sky-950); border: 1px solid var(--line); border-radius: 8px; color: var(--ink);
  padding: .7rem .9rem; font: inherit; width: 100%;
}
#psPointsClaimForm input[type="date"], #psPointsClaimForm input[type="time"] { color-scheme: dark; }
#psReferFriendForm input::placeholder,
#psAccountCreateForm input::placeholder, #psAccountLoginForm input::placeholder,
#psAccountForgotForm input::placeholder, #psAccountSetPasswordForm input::placeholder,
#psPointsClaimForm input::placeholder { color: var(--ink-faint); }
#psReferFriendForm button[type="submit"],
#psAccountCreateForm button[type="submit"], #psAccountLoginForm button[type="submit"],
#psAccountForgotForm button[type="submit"], #psAccountSetPasswordForm button[type="submit"],
#psPointsClaimForm button[type="submit"] { align-self: center; }
#psAccountForgotForm[hidden] { display: none; }

/* The pitch page's hero — a bold gradient headline plus a few faint spark
   accents scattered around it (never more than three, never animated —
   depth without turning into a busy pattern). */
/* isolation:isolate (not overflow:hidden) keeps the glow/sparks' z-index:0
   scoped behind this hero's own text without clipping them — overflow:hidden
   was cropping the glow's gradient hard at the hero's own content height
   (it's a fixed 380px tall, well past how tall the hero's text block is) and
   fully cropping the "tiny" spark, which sits at bottom:-6px on purpose so
   it peeks past the hero's edge. Document order still paints this hero (and
   anything of it that spills past its own box) behind whatever section
   comes after it, same as any two normal-flow siblings. */
.ps-pitch-hero { position: relative; isolation: isolate; text-align: center; padding-top: .5rem; }
/* An irregular soft blob, not a plain circle — took several wrong turns
   to actually land here, the last one a real mechanical bug, not a
   taste call. First cut: clip-path straight to a crisp 8-point
   sparkle/diamond, no blur — a hard-edged sticker cutout, not a glow.
   Second: a 12-point organic outline (after a gradient-blob.yuvalkarif.com
   reference), blurred by 20+px — still read as basically round
   ("what I see rendered on most of the site except rewards is still
   just the standard round glow"). Cutting the blur and going to fewer,
   bigger lobes (below) *still* didn't fix it, which is what exposed the
   real bug: the radial-gradient underneath was written as
   "transparent 70%" — meaning it fades to fully invisible by 70% of the
   way from center to the box's edge. This clip-path's own lobes reach
   out to 100%. So the entire outer 30% of the box — exactly the region
   where the polygon's lobes actually diverge from a circle — was
   painting at zero opacity regardless of what shape it was clipped to.
   All that was ever visible was the gradient's own inner, perfectly
   circular falloff. No amount of changing the *clip* shape could have
   fixed that; the fix is "transparent 92%" instead, so the gradient
   still has real brightness left out where the lobes actually are.
   The blur is what keeps the result reading as light rather than a
   shape pasted on top of the page — applied to a wrapper one level out
   (.ps-glow-wrap), not this element directly, because filter:blur() on
   the same element as clip-path blurs *then* clips straight back to
   the polygon's original hard edge, undoing itself. .ps-page-glow below
   uses the identical polygon/gradient/wrapper approach, just a lighter
   blur to match its smaller box. */
.ps-glow-wrap { position: absolute; inset: -30% -10% auto -10%; height: 380px; z-index: 0; filter: blur(16px); pointer-events: none; }
.ps-pitch-glow {
  position: absolute; inset: 0;
  background: radial-gradient(closest-side, rgba(92,139,214,.5), transparent 92%);
}
/* Three hand-drawn amoeba outlines, not one shape reused everywhere —
   Chad: "each shape should also be unique not each being the same."
   The previous shape put 8 points at perfectly even 45° spacing, which
   is exactly why it read as "a square stretched" rather than organic —
   a regular grid of points, even with varied radii, still traces a
   shape with the same rotational symmetry as the grid itself. These
   use irregular angle spacing *and* irregular radii together (closer
   to how gradient-blob.yuvalkarif.com's own reference actually works —
   real scattered points, not points placed at even angles), plus more
   of them (10-12 instead of 8) so each straight edge is shorter and the
   outline reads as a smooth curve instead of a faceted polygon. Applied
   as a modifier alongside .ps-pitch-glow/.ps-page-glow rather than
   baked into either of those, so the same shape can rotate across
   every glow spot on the site — see pageGlowHtml()/the hero markup in
   public-site.js for which variant lands where. */
.ps-blob-a { clip-path: polygon(92% 50%, 77% 63%, 76% 88%, 47% 86%, 18% 89%, 20% 64%, 7% 43%, 28% 32%, 34% 5%, 56% 16%, 79% 15%, 80% 39%); }
.ps-blob-b { clip-path: polygon(97% 58%, 72% 76%, 65% 91%, 45% 79%, 15% 79%, 19% 55%, 3% 33%, 30% 21%, 43% 9%, 62% 25%, 89% 23%, 82% 44%); }
.ps-blob-c { clip-path: polygon(95% 62%, 66% 78%, 42% 97%, 22% 69%, 6% 54%, 24% 35%, 29% 5%, 53% 14%, 74% 16%, 76% 41%); }
.ps-pitch-spark { position: absolute; z-index: 0; color: var(--caution); opacity: .5; width: 20px; height: 20px; display: block; }
.ps-pitch-spark svg { width: 100%; height: 100%; display: block; }
.ps-pitch-spark.small { width: 14px; height: 14px; }
.ps-pitch-spark.tiny { width: 10px; height: 10px; }
/* Everything in the hero except the glow/spark decorations (those need
   to stay position:absolute, out of flow) sits above them via z-index. */
/* .ps-glow-wrap is now the hero's actual direct child (it wraps
   .ps-pitch-glow, which used to sit directly here before the blur/
   clip-path split) — without excluding it too, this rule was forcing
   position:relative onto it, which doesn't just fail to position it,
   it silently reinterprets its inset:-30% -10% auto -10% as a
   *relative* offset from its own normal-flow position instead of an
   absolute one from the hero's edges — a big shift up and to the left
   of dead center, exactly matching what Chad saw ("the first one just
   has a glow up and to the left of the word. that makes no sense"). */
.ps-pitch-hero > *:not(.ps-pitch-glow):not(.ps-pitch-spark):not(.ps-glow-wrap) { position: relative; z-index: 1; }
.ps-pitch-h1 {
  font-family: var(--display); font-size: clamp(2.4rem, 6vw, 4rem); line-height: 1.05;
  text-transform: uppercase; letter-spacing: .01em; text-wrap: balance;
  background: linear-gradient(180deg, #fff, var(--ink-dim) 150%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.ps-pitch-sub { display: block; color: var(--ink-dim); max-width: 560px; margin: 1.1rem auto 0; font-size: 1.05rem; }

/* "How it works," as icon cards rather than plain text — a numbered label
   (this genuinely is a 3-step sequence, so the ordinal carries real
   information) plus a small line icon per step instead of emoji. Chad:
   "replace the step one step two etc with larger text numbers and move
   the [rest] to the right of the number that will be left aligned" — was
   a small uppercase "STEP ONE" label stacked above the icon; the number
   itself is now the big visual anchor (a plain digit, not spelled out —
   at this size "One"/"Two"/"Three" would run noticeably longer than
   "1"/"2"/"3" and throw off the row's left edge). Went through two more
   rounds after that: text beside the number with the icon trailing it
   (rejected — Chad: "text should be below the line that the number and
   icon are on"). Landed here: .ps-pitch-step-top holds just the number
   (left) and icon (right, pushed there by the number's own margin-right:
   auto) as one row, then the heading and paragraph flow in normal block
   order underneath, flush with the card's left padding — same left edge
   the number sits on, not indented to start after it. */
.ps-pitch-steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 1.1rem; margin: 3rem auto; max-width: var(--wrap); }
.ps-pitch-step {
  background: var(--sky-800); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.6rem 1.4rem; text-align: left; position: relative; overflow: hidden;
}
.ps-pitch-step::before {
  content: ''; position: absolute; top: -30%; right: -20%; width: 130px; height: 130px; border-radius: 50%;
  background: radial-gradient(circle, rgba(92,139,214,.15), transparent 70%);
}
.ps-pitch-step-top { display: flex; align-items: center; margin-bottom: .9rem; }
.ps-pitch-step-num {
  font-family: var(--display); color: var(--spark-bright); font-size: 2.4rem; line-height: 1;
  margin-right: auto;
}
.ps-pitch-step:last-child .ps-pitch-step-num { color: var(--caution-bright); }
.ps-pitch-step-icon {
  width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center;
  background: var(--sky-700); color: var(--spark-bright); flex-shrink: 0;
}
.ps-pitch-step-icon svg { width: 22px; height: 22px; }
.ps-pitch-step:last-child .ps-pitch-step-icon { color: var(--caution-bright); }
.ps-pitch-step h3 { font-size: 1.1rem; text-transform: uppercase; letter-spacing: .01em; margin-bottom: .4rem; }
.ps-pitch-step p { color: var(--ink-dim); font-size: .9rem; }

/* The referral reward, now a flat $X per friend up to a cap rather than
   an escalating ladder — Chad: "Users will now earn $5 for every friend
   that signs up and verifies up to 10 friends." Replaces both the old
   connected-node ladder (/refer) and the grouped checklist (/rewards, its
   own neon-per-batch accent colors and all) — both existed specifically
   to represent an UNEVEN structure (sparse milestones, unevenly-sized
   payout batches), which a flat rate has no version of. A first pass drew
   this as a row of 10 identical coin badges; Chad: "Not liking how the
   section looks... let me see some options" — shown four side by side,
   picked this one: a single filled progress track instead, same visual
   recipe (gradient, height, radius) as .ps-refer-progress on the account
   page, so it reads as an established pattern rather than a new one-off.
   640px max-width, narrower than .ps-status-tiles' 760px — a slim bar
   stretched to 760px read as too thin/sparse; this is its own "narrow
   module" width, not matched to the tile blocks above it. */
.ps-refflat { max-width: 640px; margin: 2.6rem auto 0; text-align: center; }
.ps-refflat-track-head { display: flex; justify-content: space-between; font-size: .9rem; color: var(--ink-dim); margin-bottom: .5rem; }
.ps-refflat-track-max { font-family: var(--head); font-weight: 700; color: var(--caution-bright); }
.ps-refflat-track {
  height: 10px; border-radius: 999px; background: var(--sky-900); border: 1px solid var(--line); overflow: hidden;
}
.ps-refflat-track-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--spark-bright), var(--caution-bright)); }
.ps-refflat-ticks { display: flex; justify-content: space-between; margin-top: .45rem; font-size: .72rem; color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.ps-refflat-cap { margin: 1.2rem 0 0; font-size: .82rem; color: var(--ink-faint); }

/* The status ladder — four tiles rather than a connected track (Chad's
   pick out of four options shown side by side), each tinted via its own
   --tier-color (set inline per tile, from STATUS_COLOR_HEX). */
.ps-status-tiles {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;
  max-width: 760px; margin: 2.6rem auto 0;
}
.ps-status-tile {
  background: var(--sky-800); border: 1px solid var(--line); border-radius: var(--radius);
  padding: 1.4rem 1.1rem; text-align: center; position: relative; overflow: hidden;
}
.ps-status-tile::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: var(--tier-color); }
.ps-status-tile-name {
  font-family: var(--head); font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
  font-size: .95rem; color: var(--tier-color); margin-top: .3rem;
}
.ps-status-tile-mult { font-family: var(--display); font-size: 2rem; margin: .6rem 0 .2rem; color: var(--ink); transition: transform .5s ease, filter .4s ease; }
.ps-status-tile-req { font-size: .78rem; color: var(--ink-faint); }

/* The account page's own "Redeem Your Points" cards only — Chad found a
   3D tilt/glow button animation online and asked for it specifically
   there, not the plain read-only tiles /rewards shows. Keeps
   .ps-status-tile's own layout/colors as-is and layers the effect on
   top: perspective lives on the card itself, the actual rotateX/Y
   transform on a .ps-status-tile-3d wrapper one level in (perspective
   and the transform can't be the same element, same split the
   reference's .btn-3d/.inner used) — pointsRedeemHtml() in
   public-site.js is what wraps each card's content in that div.
   overflow:visible (the base tile is overflow:hidden, to clip its own
   ::before accent bar to the rounded corners) is needed here so the
   tilt and its glow aren't clipped square by the un-rotated card
   underneath — the accent bar gets its own top-corner radius instead so
   it still looks clipped without relying on the parent for it. Reads
   --tier-color for the glow, the same variable that already colors a
   card gold when affordable and blue when locked, so no separate accent
   plumbing was needed to make the glow match. */
.ps-status-tile.ps-redeem-tile { overflow: visible; perspective: 900px; }
.ps-status-tile.ps-redeem-tile::before { border-radius: var(--radius) var(--radius) 0 0; }
.ps-redeem-tile .ps-status-tile-3d {
  transform-style: preserve-3d; border-radius: var(--radius);
  transition: transform .4s cubic-bezier(.18,.9,.32,1), box-shadow .3s;
}
.ps-redeem-tile:hover .ps-status-tile-3d {
  transform: rotateX(10deg) rotateY(-8deg) translateY(-6px);
  box-shadow: 0 16px 28px rgba(0,0,0,.5), 0 0 20px -4px var(--tier-color);
}
/* Only a card with a real Redeem button (affordable) gets the "this is
   interactive" cues on hover/click — a locked card still tilts for
   consistent depth across the grid, but doesn't scale/glow its own
   amount or pulse, since there's nothing to click yet. */
.ps-redeem-tile.affordable:active .ps-status-tile-3d { transform: translateY(2px) scale(.97); }
.ps-redeem-tile.affordable:hover .ps-status-tile-mult { transform: scale(1.15); filter: drop-shadow(0 0 8px var(--tier-color)); }
.ps-redeem-tile.pulse .ps-status-tile-mult { animation: ps-tile-pulse .5s ease, ps-tile-glow .5s ease; }
@keyframes ps-tile-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } }
@keyframes ps-tile-glow { 0%, 100% { filter: drop-shadow(0 0 0 transparent); } 50% { filter: drop-shadow(0 0 12px var(--tier-color)); } }
@media (prefers-reduced-motion: reduce) {
  .ps-redeem-tile .ps-status-tile-3d, .ps-status-tile-mult { transition: none; animation: none; }
  .ps-redeem-tile:hover .ps-status-tile-3d { transform: none; }
}

.ps-refer-toggle { font-size: .9rem; margin: 1.2rem 0 0; }
.ps-refer-toggle a { color: var(--ink-dim); }

/* Progress toward the next tier — a real filled bar alongside the arrow
   ladder above, not a replacement for it. */
.ps-refer-progress {
  max-width: 420px; height: 10px; margin: .8rem auto; border-radius: 999px;
  background: var(--sky-900); border: 1px solid var(--line); overflow: hidden;
}
.ps-refer-progress-fill {
  height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--spark-bright), var(--caution-bright));
  transition: width .5s ease;
}
@media (prefers-reduced-motion: reduce) { .ps-refer-progress-fill { transition: none; } }

/* Step 2 of the friend-signup form, revealed in place once a code has
   been texted — see wireReferFriendForm() in public-site.js. */
.ps-refer-code-section { display: flex; flex-direction: column; gap: .5rem; padding-top: .4rem; border-top: 1px dashed var(--line); }
.ps-refer-code-section[hidden] { display: none; }
.ps-refer-code-section .ps-refer-toggle { margin: 0; font-size: .82rem; text-align: center; }

.h-captcha { margin: .2rem auto; display: flex; justify-content: center; }

.ps-refer-count { font-family: var(--head); font-size: 1.15rem; margin: 0 0 .4rem; }

.ps-refer-codes { max-width: 480px; margin: 1rem auto; display: flex; flex-direction: column; gap: .6rem; }
.ps-refer-code-row {
  display: flex; align-items: center; justify-content: space-between; gap: .8rem; flex-wrap: wrap;
  background: var(--sky-900); border: 1px solid var(--line); border-radius: 8px; padding: .7rem 1rem;
}
.ps-refer-code-row .ps-newsletter-reward.ps-newsletter-code { margin: 0; padding: .3rem .8rem; font-size: 1.1rem; border-style: solid; }
.ps-refer-code-row .amount { font-family: var(--head); color: var(--ink); }
.ps-refer-code-badge {
  font-size: .72rem; text-transform: uppercase; letter-spacing: .04em; font-weight: 700;
  padding: .2rem .6rem; border-radius: 999px;
}
.ps-refer-code-badge.unredeemed { background: rgba(246, 185, 59, .18); color: var(--caution-bright); }
.ps-refer-code-badge.redeemed { background: rgba(79, 191, 139, .18); color: var(--ok); }

/* The referrer's one shared Square gift card (sharedGiftCardHtml in
   public-site.js) — its own richer card, not a plain .ps-refer-code-row,
   since it's real spendable money: Chad, after reviewing barcode-reveal
   mockups, "I like option B" (this card, not a ticket or a
   receipt) and picked the colored-ID-strip footer treatment below. Same
   --tier gradient language as .ps-member-card, tinted to the customer's
   own status color so it reads as part of the same account, not a
   bolted-on component. */
.ps-giftcard {
  position: relative; overflow: hidden; border-radius: 16px; padding: 1.2rem 1.25rem 1.1rem;
  text-align: center;
  background: linear-gradient(150deg, color-mix(in srgb, var(--tier) 22%, var(--sky-800)), var(--sky-900) 62%);
  border: 1px solid color-mix(in srgb, var(--tier) 40%, transparent);
  box-shadow: 0 10px 30px -18px color-mix(in srgb, var(--tier) 80%, transparent);
}
.ps-giftcard::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 80% at 50% 0%, color-mix(in srgb, var(--tier) 20%, transparent), transparent 60%);
}
.ps-giftcard > * { position: relative; z-index: 1; }
.ps-giftcard-eyebrow { font-family: var(--head); font-weight: 700; text-transform: uppercase; letter-spacing: .14em; font-size: .66rem; color: var(--ink-faint); }
.ps-giftcard-amount { font-family: var(--display); font-size: 2rem; line-height: 1; color: var(--ink); margin: .6rem 0 .2rem; }
.ps-giftcard-sub { font-size: .76rem; color: var(--ink-faint); margin-bottom: .2rem; }
.ps-giftcard-check { font-size: 1.7rem; margin: .2rem 0; }

/* The pre-claim "ready" row — Chad: "Prior to claiming the card should
   have a left alignment, once they go to claim it should all be center
   aligned." Its own compact, left-aligned layout (picked "A" — Compact
   Left Row — out of three options), distinct from .ps-giftcard above:
   everything from confirm-reveal onward stays that taller, centered
   card; only this one everyday sitting-in-the-list state is a row. */
.ps-giftcard-ready {
  display: flex; align-items: center; justify-content: space-between; gap: .8rem; flex-wrap: wrap;
  background: linear-gradient(150deg, color-mix(in srgb, var(--tier) 16%, var(--sky-900)), var(--sky-900) 70%);
  border: 1px solid color-mix(in srgb, var(--tier) 35%, transparent); border-radius: 8px;
  padding: .7rem 1rem;
}
.ps-giftcard-ready-amount { font-family: var(--display); font-size: 1.5rem; line-height: 1; color: var(--ink); margin: .45rem 0 .1rem; }
.ps-giftcard-ready-sub { font-size: .72rem; color: var(--ink-faint); }

/* Both warning steps (confirm-reveal, confirm-done) share this shell. */
.ps-giftcard-warn { margin: 1rem -1.25rem -1.1rem; padding: 1rem 1.25rem 1.1rem; background: rgba(0, 0, 0, .32); border-top: 1px solid var(--line); text-align: center; }
.ps-giftcard-warn .head { margin-bottom: .5rem; }
.ps-giftcard-warn .ico { display: block; font-size: 1.3rem; margin-bottom: .3rem; }
.ps-giftcard-warn .title { display: block; font-family: var(--head); font-weight: 700; text-transform: uppercase; letter-spacing: .03em; font-size: .82rem; color: var(--ember-bright); }
.ps-giftcard-warn .body-text { font-size: .76rem; color: var(--ink-dim); line-height: 1.45; margin-bottom: .8rem; }
.ps-giftcard-warn .ps-btn + .ps-btn { margin-top: .5rem; }

/* Bars are drawn black with no fill behind them (code128.js), so this
   needs its own light plate regardless of the card's own dark theme —
   both for contrast and because a 1D scanner expects dark bars on a
   light ground. */
.ps-giftcard-barcode { background: #fff; border-radius: 8px; padding: .8rem .6rem .5rem; margin-top: 1rem; }
.ps-giftcard-barcode svg { width: 100%; max-width: 260px; height: auto; display: block; margin: 0 auto; }
.ps-giftcard-barcode .gan { font-family: monospace; font-size: .74rem; letter-spacing: .08em; color: #111; margin-top: .35rem; }
.ps-giftcard-expiry { font-size: .68rem; color: var(--ink-faint); margin-top: .55rem; }

/* The colored ID strip — Chad's pick out of three footer treatments,
   specifically for being readable by staff at a glance, not just the
   customer. Text is fixed dark-on-tier, which only works because every
   status tier seeded today (points.js's STATUS_COLORS) reads fine with
   it; a genuinely dark custom tier color would need this computed from
   the tier color's own lightness instead — flagged, not solved here. */
.ps-giftcard-idstrip { margin: 1.1rem -1.25rem -1.1rem; padding: 1.1rem 1.25rem 1.2rem; text-align: center; border-radius: 0 0 15px 15px;
  background: linear-gradient(155deg, var(--tier), color-mix(in srgb, var(--tier) 75%, #000)); }
.ps-giftcard-idstrip .nm { font-family: var(--display); font-size: 1.25rem; color: #1d1400; line-height: 1.2; }
.ps-giftcard-idstrip .status { display: block; margin-top: .4rem; font-family: var(--head); font-weight: 700; text-transform: uppercase; letter-spacing: .1em; font-size: .78rem; color: rgba(0, 0, 0, .62); }

.ps-giftcard-notice { font-size: .78rem; color: var(--ink-faint); line-height: 1.5; margin-top: .5rem; }

/* Chad: "once it sees its redeemed it should show that redeem page
 * without the slash over the dollar amount left" — the one-time
 * just-redeemed confirmation (see sharedGiftCardHtml's own doc comment)
 * keeps the green accent glow but not the previous strikethrough. */
.ps-giftcard.just-redeemed { border-color: color-mix(in srgb, var(--ok) 45%, transparent); box-shadow: 0 10px 30px -18px color-mix(in srgb, var(--ok) 60%, transparent); }
.ps-giftcard.just-redeemed::after { background: radial-gradient(120% 80% at 50% 0%, color-mix(in srgb, var(--ok) 18%, transparent), transparent 60%); }
/* Status tier badge on the Points tab — --status-color is set inline per
   tier (points.js's STATUS_COLORS), same idiom as the redemption badges
   just above using a fixed color per state. */
.ps-status-badge {
  display: inline-flex; align-items: center; gap: .4rem;
  font-family: var(--head); font-weight: 700; text-transform: uppercase; letter-spacing: .04em; font-size: .82rem;
  padding: .3rem .8rem; border-radius: 999px;
  background: color-mix(in srgb, var(--status-color) 18%, transparent);
  color: var(--status-color); border: 1px solid color-mix(in srgb, var(--status-color) 45%, transparent);
}
/* A newly-earned, unredeemed code gets a little more presence than a
   flatly-listed row — a soft gold glow, a small "reward unlocked" moment,
   not a full modal. */
.ps-refer-code-row.ready { border-color: var(--caution-bright); box-shadow: 0 0 0 1px rgba(246, 185, 59, .3), 0 0 24px -8px rgba(246, 185, 59, .5); }

.ps-refer-share {
  max-width: 480px; margin: 1.2rem auto 0; display: flex; align-items: center; justify-content: center; gap: .8rem; flex-wrap: wrap;
  background: var(--sky-950); border: 1px dashed var(--line); border-radius: 8px; padding: .8rem 1rem;
}
.ps-refer-share-url { font-family: monospace; font-size: .85rem; color: var(--ink-dim); word-break: break-all; }

/* The pitch page's closing call to action — a primary button straight to
   account creation, plus a plain "already have one" toggle underneath. */
.ps-refer-cta { max-width: 420px; margin: 2rem auto 0; display: flex; flex-direction: column; align-items: center; gap: .6rem; }

/* The hero's glow/spark motif, carried down the rest of a page (Chad,
   originally on /rewards: "stars and glow in some capacity all the way
   down the page" — then again: "let's add the stars and glow to safety
   page also," then to /locations and the home page from "Shop By
   Category" (just below the brand strip) down through the rest of that
   page — "not just the category section," once the first pass only
   covered that one). First attempt on /rewards
   scattered sparks/glows across the whole page by height percentage
   against the section — but everything below its hero is full-width
   tiles or checklist rows with their own opaque backgrounds, so almost
   every one of them just painted underneath a card and vanished (Chad:
   "I don't see it growing the full length or any additional stars
   added"). Anchored to each section heading instead — .ps-safety-head
   (on /rewards and /safety), .ps-section-head (the plainer heading
   /locations and most other pages use, including that one home-page
   section), plus /rewards' own closing .ps-refer-cta — those are the
   one spot per section that's full section width but only its
   *centered text* actually fills it, leaving the open thirds on either
   side clear at any viewport width. position:relative on
   .ps-section-head here is otherwise a no-op (every page using it
   without any spark/glow children renders identically) — only the
   handful of call sites that actually pass sectionSparks()/add
   .ps-page-glow get the decoration. .ps-deco-page's isolation:isolate
   scopes the sparks/glow's z-index:-1 to just that page's own section;
   negative z-index alone is enough to sit them behind every real
   element there without having to touch any of that content's own
   stacking (the inverse of the hero's own fix, where the decorations
   stayed at z-index:0 and the hero's *text* got bumped up to z-index:1
   instead — bumping dozens of unrelated tiles/rows/cards up would have
   meant touching all of them). */
.ps-deco-page { position: relative; isolation: isolate; }
.ps-safety-head, .ps-refer-cta, .ps-section-head { position: relative; }
.ps-page-spark { position: absolute; z-index: -1; color: var(--caution); opacity: .35; width: 16px; height: 16px; display: block; }
.ps-page-spark svg { width: 100%; height: 100%; display: block; }
.ps-page-spark.small { width: 11px; height: 11px; }
.ps-page-spark.tiny { width: 8px; height: 8px; }
/* Same blur-goes-on-a-wrapper-not-the-clipped-element split as the
   hero's own .ps-glow-wrap/.ps-pitch-glow (see that comment above) — a
   differently-named wrapper since this one needs centered-square
   positioning instead of the hero's wide inset box. */
.ps-page-glow-wrap {
  position: absolute; z-index: -1; top: 50%; left: 50%; width: 420px; height: 420px;
  transform: translate(-50%, -50%); filter: blur(13px); pointer-events: none;
}
/* /rewards' own closing CTA (Chad: "the one at the bottom of the
   rewards page is a little large, they shouldn't spill too heavily
   into the next section") sits right above the footer with much less
   surrounding breathing room than a section heading gets from its own
   prose/margins — the full 420px size was reaching past the button
   into the footer text below it. */
.ps-page-glow-wrap.small { width: 300px; height: 300px; }
.ps-page-glow {
  position: absolute; inset: 0;
  background: radial-gradient(closest-side, rgba(92,139,214,.4), transparent 92%);
}

/* ---------------- account hub (/account) ---------------- */

/* Logged out: create-account + log-in side by side (stacking on mobile),
   same grid idiom .ps-contact-cards already uses for its own card rows. */
.ps-account-columns {
  max-width: 900px; margin: 0 auto; display: grid; gap: 1.3rem;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); align-items: start;
}
.ps-account-columns .ps-contact-form-wrap { margin: 0; }
/* A little more presence than a plain form card — an account is a
   standing relationship, not a one-off contact form. */
.ps-account-card { position: relative; }
.ps-account-card-sub { color: var(--ink-faint); font-size: .85rem; margin: -.6rem 0 1.3rem; text-align: center; }
.ps-account-divider {
  display: flex; align-items: center; gap: .8rem; margin: 1.2rem 0; color: var(--ink-faint);
  font-size: .78rem; text-transform: uppercase; letter-spacing: .08em;
}
.ps-account-divider::before, .ps-account-divider::after { content: ''; flex: 1; height: 1px; background: var(--line); }

/* .ps-account-tabs is just the (optionally scrolling) frame; the actual
   flex row lives one level in, at .ps-account-tabs-inner, sized to its
   own content (width:max-content) and auto-margined within the frame.
   That combination is what centers the row whenever it fits and safely
   falls back to flush-left whenever it doesn't (auto margins compute to
   0 the moment there's no spare space to split), on any browser, with
   no scroll-position surprises: justify-content:center directly on an
   overflow:auto flex container was tried first, including the
   safe/unsafe keyword meant for exactly this, but Chromium still opened
   the row pre-scrolled toward the end, clipping the *first* tab's icon
   off-screen at load — the opposite of what "safe" is supposed to
   prevent. Deliberately no justify-content and no max-width here on the
   inner row itself — width:max-content already sizes it to exactly fit
   its own (flex-shrink:0) children with zero spare space to justify,
   and a max-width:100% clamp (tried, then removed) shrank this box back
   below that content width the moment the row overflowed, which just
   reintroduced the same centered-overflow clipping one level deeper. */
.ps-account-tabs { max-width: 560px; margin: 0 auto 1.6rem; border-bottom: 1px solid var(--line); }
.ps-account-tabs-inner { display: flex; gap: 1.8rem; width: max-content; margin: 0 auto; }
.ps-account-tab {
  background: none; border: none; color: var(--ink-dim); font-family: var(--head); font-weight: 600;
  font-size: .92rem; letter-spacing: .02em; text-transform: uppercase; padding: .7rem .1rem;
  display: flex; align-items: center; gap: .5rem;
  border-bottom: 2px solid transparent; margin-bottom: -1px; cursor: pointer; transition: color .15s, border-color .15s;
}
/* Four tabs don't fit an iPhone-width row at this gap/padding — flex's
   default no-wrap was squeezing every label and its icon down until
   they wrapped mid-word, and the whole thing sat flush against both
   edges of the screen with no breathing room. Below that width it
   becomes a horizontally-scrolling strip instead (a standard mobile
   tab-bar pattern) — bleeding edge to edge on purpose, cancelling
   .ps-section's own side padding, with an affordance fade so a hidden
   tab doesn't just end at a hard edge. overflow-y:hidden alongside
   overflow-x:auto matters here, not just belt-and-suspenders: setting
   only one axis to a non-visible value makes browsers treat the other
   as auto too (CSS Overflow §3), so without it this strip was also
   draggable/scrollable vertically — Chad: "if it stays scrollable it
   should only scroll left and right and be locked vertically." Whether
   the row actually needs to scroll here depends only on
   .ps-account-tabs-inner's own width:max-content vs. this frame — see
   the comment above it. */
@media (max-width: 560px) {
  .ps-account-tabs {
    max-width: none; margin-left: -1.5rem; margin-right: -1.5rem; padding: 0 1.5rem;
    overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 1.5rem, #000 calc(100% - 1.5rem), transparent);
    mask-image: linear-gradient(90deg, transparent, #000 1.5rem, #000 calc(100% - 1.5rem), transparent);
  }
  .ps-account-tabs::-webkit-scrollbar { display: none; }
  .ps-account-tabs-inner { gap: 1.4rem; }
  .ps-account-tab { flex-shrink: 0; white-space: nowrap; }
  .ps-account-tab[data-tab="preferences"] .ps-account-tab-label { display: none; }
  .ps-account-tab[data-tab="preferences"] .ps-account-tab-label-short { display: inline; }
}
.ps-account-tab-label-short { display: none; }
.ps-account-tab svg { width: 17px; height: 17px; flex-shrink: 0; }
.ps-account-tab:hover { color: var(--ink); }
.ps-account-tab.active { color: var(--spark-bright); border-color: var(--spark-bright); }

/* referrals tab — headline numbers before the detail */
.ps-stat-row { display: flex; justify-content: center; gap: 3rem; text-align: center; margin-bottom: 1.5rem; flex-wrap: wrap; }
.ps-stat-num { font-family: var(--display); font-size: 2rem; color: var(--ink); }
.ps-stat-lbl { font-size: .76rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .06em; margin-top: .3rem; }
/* The Points tab's member card and the rank ladder under it — Chad
   picked this pairing ("option 1, card, ladder underneath") out of four
   card/ladder mixes shown side by side. --tier is the customer's own
   status color (STATUS_COLOR_HEX, the hue their Wallet pass strip is
   baked in), set inline per render; --status-color rides along on the
   same element so the tier chip can be .ps-status-badge verbatim rather
   than a second pill that only differs by name. */
.ps-member-card {
  position: relative; overflow: hidden; max-width: 460px; margin: 0 auto 1.1rem;
  border-radius: 16px; padding: 1.25rem 1.35rem 1.15rem;
  background: linear-gradient(150deg, color-mix(in srgb, var(--tier) 22%, var(--sky-800)), var(--sky-900) 62%);
  border: 1px solid color-mix(in srgb, var(--tier) 40%, transparent);
  box-shadow: 0 10px 30px -18px color-mix(in srgb, var(--tier) 80%, transparent);
  text-align: left;
}
/* A soft bloom off the top-right corner, so the tint reads as light
   falling on a card rather than a flat wash of color. */
.ps-member-card::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(120% 80% at 100% 0%, color-mix(in srgb, var(--tier) 20%, transparent), transparent 60%);
}
.ps-member-card > * { position: relative; z-index: 1; }
.ps-member-top { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.ps-member-brand {
  font-family: var(--head); font-weight: 700; text-transform: uppercase;
  letter-spacing: .14em; font-size: .72rem; color: var(--ink-faint);
}
/* wrap, not shrink: at a narrow width the unit drops under the number
   instead of being clipped by the card's own overflow: hidden. */
.ps-member-bal { margin: 1rem 0 .2rem; display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; row-gap: .1rem; }
.ps-member-num { font-family: var(--display); font-size: 2.8rem; line-height: 1; color: var(--ink); }
.ps-member-unit {
  font-family: var(--head); font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; font-size: .88rem; color: var(--ink-dim); white-space: nowrap;
}
/* The whole point of the card: lifetime is a line of small print under
   the spendable balance, never a second number the same size as it. */
.ps-member-life { font-size: .8rem; color: var(--ink-faint); margin: 0 0 1rem; }
.ps-member-life b { color: var(--ink-dim); font-weight: 600; }
.ps-member-foot {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 1rem; row-gap: .6rem; flex-wrap: wrap; padding-top: .85rem; border-top: 1px solid var(--line);
}
.ps-member-name {
  display: block; font-family: var(--head); font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; font-size: .98rem; color: var(--ink);
}
.ps-member-since { display: block; font-size: .74rem; color: var(--ink-faint); margin-top: .15rem; }
.ps-member-rate { text-align: right; flex-shrink: 0; }
.ps-member-rate b { display: block; font-family: var(--display); font-size: 1.35rem; color: var(--tier); line-height: 1; }
.ps-member-rate span {
  display: block; font-size: .68rem; text-transform: uppercase;
  letter-spacing: .08em; color: var(--ink-faint); margin-top: .2rem;
}

/* Progress to the next reward, as a strip along the card's bottom edge
   (Chad picked this placement out of four). The negative margins are the
   card's own padding inverted — 1.35rem at the sides and the full 1.15rem
   at the bottom — so the strip runs edge to edge and sits flush on the
   card's bottom border without the card needing a modifier class. */
.ps-member-strip {
  margin: 1rem -1.35rem -1.15rem; padding: .6rem 1.35rem .7rem;
  background: rgba(0, 0, 0, .28); border-top: 1px solid var(--line);
}
.ps-member-strip-row { display: flex; align-items: baseline; justify-content: space-between; gap: .8rem; margin-bottom: .4rem; }
.ps-member-strip-lbl {
  font-family: var(--head); font-weight: 700; text-transform: uppercase;
  letter-spacing: .08em; font-size: .68rem; color: var(--ink-faint);
}
.ps-member-strip-val { font-size: .78rem; color: var(--ink-dim); text-align: right; }
.ps-member-strip-val b { color: var(--ink); font-weight: 700; }
/* Its own slim bar rather than .ps-refer-progress — that one is sized for
   a full-width block on the page ground, and its --sky-900 fill would
   disappear against the darkened strip it sits on here. */
.ps-member-strip-bar { height: 5px; border-radius: 999px; background: rgba(0, 0, 0, .45); overflow: hidden; }
.ps-member-strip-bar > i {
  display: block; height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--spark-bright), var(--caution-bright));
  transition: width .5s ease;
}
@media (prefers-reduced-motion: reduce) { .ps-member-strip-bar > i { transition: none; } }

/* The share link as a band inside the Referrals card — Chad: "I like the
   referral link and code being in a card". Deliberately the same dashed
   outline .ps-refer-share already uses for the standalone share box on
   /refer, so it still reads as "the thing you copy" once it's sitting on
   a tinted card instead of the page ground. */
.ps-code-band {
  display: flex; align-items: center; justify-content: space-between; gap: .7rem; flex-wrap: wrap;
  margin: .95rem 0 0; padding: .6rem .75rem; border-radius: 8px;
  border: 1px dashed rgba(255, 255, 255, .22); background: rgba(0, 0, 0, .22);
}
.ps-code-band-code { font-family: monospace; font-size: 1rem; letter-spacing: .1em; color: var(--ink); }
.ps-code-band-sub {
  display: block; font-family: var(--body); font-size: .66rem; letter-spacing: 0;
  color: var(--ink-faint); margin-top: .15rem; word-break: break-all;
}

/* Rank ladder. Every rung is an equal 1/n slice of the row with its dot
   centered in it, which is what lets pointsRankRailHtml() inset the rail
   by half a rung and then treat the fill as a plain rung-to-rung
   percentage. min-width: 0 + ellipsis so a long tier name (or a ladder
   with more rungs than the four configured today) degrades by
   truncating rather than colliding with its neighbour. */
.ps-rank { max-width: 460px; margin: 0 auto 1.6rem; }
.ps-rank-rungs { position: relative; display: flex; justify-content: space-between; }
.ps-rank-rail {
  position: absolute; top: 9px; height: 4px; border-radius: 999px;
  background: var(--sky-900); border: 1px solid var(--line); overflow: hidden;
}
.ps-rank-rail-fill {
  height: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--spark-bright), var(--caution-bright));
  transition: width .5s ease;
}
.ps-rank-rung {
  position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center;
  gap: .35rem; flex: 1 1 0; min-width: 0; padding: 0 2px;
}
.ps-rank-dot { width: 20px; height: 20px; border-radius: 50%; background: var(--sky-900); border: 2px solid var(--sky-700); }
.ps-rank-rung.done .ps-rank-dot, .ps-rank-rung.now .ps-rank-dot { background: var(--rung); border-color: var(--rung); }
.ps-rank-rung.now .ps-rank-dot { box-shadow: 0 0 0 4px color-mix(in srgb, var(--rung) 25%, transparent); }
.ps-rank-name {
  font-family: var(--head); font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  font-size: .74rem; color: var(--ink-faint);
  white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
}
.ps-rank-rung.done .ps-rank-name { color: var(--ink-dim); }
.ps-rank-rung.now .ps-rank-name { color: var(--rung); }
.ps-rank-mult { font-size: .66rem; color: var(--ink-faint); }
.ps-rank-rung.now .ps-rank-mult { color: var(--ink-dim); }
@media (prefers-reduced-motion: reduce) { .ps-rank-rail-fill { transition: none; } }

.ps-progress-wrap { max-width: 460px; margin: 0 auto 1.6rem; }
.ps-progress-note { text-align: center; color: var(--ink-dim); font-size: .92rem; margin-top: .7rem; }

/* preferences tab — a real toggle switch and a segmented control instead
   of a bare checkbox and <select>, both still plain form controls under
   the hood (a checkbox, a radio group) so submission works exactly as
   before. */
.ps-prefs-card { background: var(--sky-900); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.7rem; max-width: 440px; margin: 0 auto; text-align: left; }
.ps-pref-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding-bottom: 1.2rem; margin-bottom: 1.2rem; border-bottom: 1px solid var(--line); cursor: pointer; }
.ps-pref-label { font-weight: 600; color: var(--ink); }
.ps-pref-desc { font-size: .82rem; color: var(--ink-faint); margin-top: .2rem; }
.ps-switch { position: relative; width: 44px; height: 26px; flex-shrink: 0; display: inline-block; }
.ps-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.ps-switch-track { position: absolute; inset: 0; background: var(--sky-700); border-radius: 999px; transition: background .15s; }
.ps-switch-track::before { content: ''; position: absolute; width: 20px; height: 20px; left: 3px; top: 3px; border-radius: 50%; background: var(--ink); transition: transform .15s; }
.ps-switch input:checked + .ps-switch-track { background: var(--ok); }
.ps-switch input:checked + .ps-switch-track::before { transform: translateX(18px); }
.ps-switch input:focus-visible + .ps-switch-track { outline: 2px solid var(--spark-bright); outline-offset: 2px; }
.ps-account-field-label { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-faint); display: block; }
.ps-segmented { display: flex; background: var(--sky-950); border: 1px solid var(--line); border-radius: 999px; padding: .25rem; margin-top: .7rem; gap: .15rem; }
/* min-width: 0 is the actual fix for the overflow itself — a flex item's
   default min-width:auto refuses to shrink it below its own content's
   width no matter what overflow rule sits on a child span, so without
   this the row just overflows its container instead of the text ever
   getting clipped/ellipsized. The overflow/ellipsis on .ps-segmented-opt
   span below is what turns that "now allowed to shrink" into "shrinks
   and truncates cleanly" instead of "shrinks and the text spills over
   the pill's own edge anyway." */
.ps-segmented-opt { flex: 1; min-width: 0; }
.ps-segmented-opt input { position: absolute; opacity: 0; width: 0; height: 0; }
/* Chad: "the opt out text doesn't line up with the other text bubbles
   and then when selected the bubble doesn't even fill the whole bar" —
   the real bug: this used to be a plain descendant selector (.ps-
   segmented-opt span), which also matched the nested .ps-segmented-label/
   -label-short spans one level down (see accountPreferencesTabHtml) and
   gave THEM their own display:block + padding too. Email/Text only ever
   show one label, so the double-padded box was invisible there, but the
   swapped option nests two label spans inside its own pill span — one
   hidden, one shown — and that inner span's own padding stacked on top
   of the outer pill's, shrinking and off-centering the visible pill
   within its row instead of filling it edge-to-edge like the others.
   `>` scopes all of this to the one real pill span per option (the
   input's direct sibling) — the label spans inside it are now unstyled
   plain text, just swapped by display, with no box of their own to
   interfere. */
.ps-segmented-opt > span {
  display: block; text-align: center; padding: .55rem .5rem; border-radius: 999px; cursor: pointer;
  font-family: var(--head); font-weight: 600; font-size: .8rem; text-transform: uppercase; letter-spacing: .02em;
  color: var(--ink-dim); transition: background .15s, color .15s;
  /* Chad: "all it did was drop the word me and still fall out of line" —
     white-space: nowrap alone (below) stops the *wrap* but a flex:1
     column can still shrink narrower than its own nowrap content, and
     with nothing clipping it that content just spills past the pill's
     own rounded edge instead of wrapping — which is exactly what the
     first pass at this did. overflow+ellipsis is the actual hard
     guarantee: whatever text is in here, it now visually ends at the
     pill's edge no matter how narrow the column gets. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ps-segmented-opt input:checked + span { background: var(--spark); color: #fff; }
.ps-segmented-opt input:focus-visible + span { outline: 2px solid var(--spark-bright); outline-offset: 2px; }
/* Chad: "on iPhone Safari the contact bar oddly goes to two lines, let's
   make sure that can't happen" — "Don't Contact Me" is the one label
   long enough to wrap in its 1/3-width pill. Swap to a short label under
   the same phone-only breakpoint .ps-cats/.ps-account-tabs already use,
   short enough to render in full rather than lean on the ellipsis above
   as anything but a last-resort backstop. Only the option(s) that
   actually have a short variant get the .ps-segmented-swap wrapper (see
   accountPreferencesTabHtml) — Email/Text have no short label to swap
   to, so they're untouched by either rule below and stay visible at
   every width. Both labels are now plain inline text (no display:block
   of their own — see the .ps-segmented-opt > span comment above). */
.ps-segmented-swap .ps-segmented-label-short { display: none; }
@media (max-width: 560px) {
  .ps-segmented-swap .ps-segmented-label { display: none; }
  .ps-segmented-swap .ps-segmented-label-short { display: inline; }
}
.ps-prefs-save { width: 100%; justify-content: center; margin-top: 1.6rem; }

/* wishlist tab — product tiles, not plain rows */
.ps-wish-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.1rem; max-width: 780px; margin: 0 auto; }
.ps-wish-tile { background: var(--sky-900); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; text-align: left; }
.ps-wish-photo {
  position: relative; aspect-ratio: 4/3; background: var(--sky-700); display: flex; align-items: center; justify-content: center;
  font-size: 2rem; overflow: hidden;
}
.ps-wish-photo img { width: 100%; height: 100%; object-fit: contain; }
.ps-wish-remove {
  position: absolute; top: .5rem; right: .5rem; width: 28px; height: 28px; border-radius: 50%; border: none;
  background: rgba(7,10,22,.72); color: #fff; cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: background .15s;
}
.ps-wish-remove:hover { background: var(--ember); }
.ps-wish-tile-body { padding: .9rem 1rem 1.1rem; }
.ps-wish-cat { font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); }
.ps-wish-name { font-weight: 600; color: var(--ink); margin: .2rem 0 .35rem; }
.ps-wish-price { font-family: var(--head); font-weight: 700; color: var(--caution-bright); font-size: .98rem; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .ps-btn, .ps-cat-card { transition: none; }
}
