/* ════════════════════════════════════════════════════════════════════════
 * dp-home.css — the media-first homepage, in two themes.
 *
 * SCOPE. This file owns the homepage's own chrome: header, hero reel,
 * Hot/Latest tabs, the card grid, the pop-out panel. The CARD itself is
 * public/css/dp-attraction-card.css and is deliberately independent of
 * everything here — same card, dark page, white page or pop-out panel.
 *
 * TWO THEMES, ONE TOKEN SET  (Aug 2026)
 * ─────────────────────────────────────
 * DARK IS THE DEFAULT. The page shipped white; the feedback was that it
 * read as a different website from /share, /profile, /download and the
 * portals, all of which are the purple register. So the dark values are
 * now the ones the page renders with, and the white ones are kept intact
 * behind an env var (HOME_THEME=light — see lib/pageTheme.js) so the two
 * can be compared on the live page and so reverting is a restart rather
 * than a deploy.
 *
 * Which one applies is a class on BOTH <html> and <body>: .dp-dark or
 * .dp-light, written by views/home.ejs. Both blocks below define exactly
 * the same six --dp-* tokens with different values, and nothing under
 * this line — nor in dp-attraction-feed.css or dp-attraction-panel.css —
 * references a literal colour. That is what keeps the themes in sync: a
 * new rule written against the tokens is correct in both by
 * construction, and a rule written against a hex is a bug in one of them.
 *
 * The class is on <html> as well because a root element with a
 * non-transparent background stops the browser propagating <body>'s
 * background to the canvas. Painting only body left Safari filling its
 * rubber-band overscroll white above a dark page. /download hit the same
 * thing from the other direction — see html.dl-light in views/download.ejs.
 *
 * WHAT NEITHER THEME TOUCHES
 * ──────────────────────────
 * public/css/mydreampark-ui.css is dark-only and this page uses its
 * components — the auth modal above all, which is what "Build Your Park"
 * eventually reaches. We do NOT restyle it and we NEVER override its
 * --mdp-ui-* tokens: those would leak into every modal on the page.
 *
 * That rule did not relax when the default went dark. The dark theme
 * COMPOSES with those tokens — it reads --mdp-ui-ink-void and
 * --mdp-ui-ink-deep to build its background, with the literals repeated
 * as fallbacks — rather than redefining them. Reading is safe; writing
 * is what leaks.
 *
 * NO TEXT IS A DESIGN CONSTRAINT, NOT A LICENCE
 * ─────────────────────────────────────────────
 * The only visible words on this page are the two buttons and the
 * Hot/Latest selector. Everything that makes the page findable and usable
 * — the <h1>, the JSON-LD, aria-labels, alt text — is present and
 * invisible. .sr-only below is load-bearing, not a leftover.
 * ════════════════════════════════════════════════════════════════════ */

/* ── Tokens that are the same in both themes ───────────────────────────
 * The brand violet, straight from .dl-mega on /download — same three
 * stops, so the two pages' CTAs are the same object. It does not flip
 * between themes: the violet IS the brand, and the only thing that
 * changes with the background is how much glow it can carry (see the
 * .dp-cta block). */
.dp-light, .dp-dark {
  --dp-violet: #7700ff;
  --dp-violet-2: #9d4edd;
  --dp-violet-3: #c77dff;

  /* THE VIOLET BUTTON'S PAINT, as one token rather than one recipe
   * copied per button. Two things wear it — the header's GET pill and,
   * on the dark page, the Discord button in the middle of the grid — and
   * they cannot share a RULE because they are different sizes and the
   * sheets load in an order that would let the small one win. They can
   * share the values. Add a third caller here, never a third gradient. */
  --dp-violet-fill:
    linear-gradient(180deg, rgba(255, 255, 255, .24) 0%, transparent 46%),
    linear-gradient(135deg, var(--dp-violet) 0%, var(--dp-violet-2) 55%, var(--dp-violet-3) 115%);
  --dp-violet-edge: rgba(199, 125, 255, .55);

  --dp-gutter: clamp(14px, 3vw, 32px);
  --dp-ease: cubic-bezier(.16, 1, .3, 1);
}

/* ── LIGHT: the original white page, now behind HOME_THEME=light ─────── */
.dp-light {
  --dp-ink: #0b0b0f;
  --dp-ink-soft: rgba(11, 11, 15, .55);
  --dp-surface: #ffffff;
  --dp-surface-2: #f6f6f8;
  --dp-line: rgba(11, 11, 15, .10);

  /* The ink a BORROWED component paints itself with — today the ghost
   * Discord button in the middle of the grid, which is site-header's
   * .dph-cta-ghost and is written against --mdp-*. dp-attraction-feed.css
   * maps these onto those; see the long note on .dp-break there for why
   * the mapping lives at that end and the values live here. */
  --dp-chrome-ink: #150a24;
  --dp-chrome-line: rgba(21, 10, 36, .18);
  --dp-chrome-line-hover: rgba(21, 10, 36, .34);
  --dp-chrome-wash: linear-gradient(to bottom, rgba(21, 10, 36, 0), rgba(21, 10, 36, .06));

  /* The Discord band. Unchanged from what --dp-surface-2 gave it. */
  --dp-break-bg: #f6f6f8;
  --dp-break-edge: transparent;
}

/* ── DARK: the default, and the register the rest of the site is in ────
 * --dp-surface / --dp-surface-2 are TRANSLUCENT here, where the light
 * theme's are opaque, and that difference is deliberate rather than an
 * oversight. They are consumed as the fill of things that sit ON the
 * page — the Show-more pill, its hover state, the Discord band, the
 * loading skeleton. On white, "a surface" means a slightly grey solid.
 * On a page whose background is a gradient with three coloured washes in
 * it, an opaque slab is a hole punched through that gradient; a white
 * veil at 6-10% is the same object rendered correctly, and it keeps the
 * wash showing through exactly as the glass panels elsewhere do.
 *
 * The body background is therefore written out below rather than taken
 * from --dp-surface the way the light theme takes it. */
.dp-dark {
  --dp-ink: #ffffff;
  --dp-ink-soft: rgba(235, 228, 245, .62);
  --dp-surface: rgba(255, 255, 255, .05);
  --dp-surface-2: rgba(255, 255, 255, .07);
  --dp-line: rgba(255, 255, 255, .14);

  /* See the light block. White ink, and a veil rather than a tint, for
   * the same reason --dp-surface is translucent here. */
  --dp-chrome-ink: #ffffff;
  --dp-chrome-line: rgba(255, 255, 255, .20);
  --dp-chrome-line-hover: rgba(255, 255, 255, .38);
  --dp-chrome-wash: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, .07));

  /* THE DISCORD BAND, and the reason it is not just a lighter veil.
   * A flat white-at-7% panel over a three-colour gradient is a grey
   * rectangle laid ON the page rather than a part of it: it desaturates
   * everything behind it and its top and bottom edges are the only hard
   * horizontals on the whole homepage. So the band is VIOLET instead,
   * and it is a gradient of its own — brightest where the button sits
   * and falling away at both edges, which is the same move the page
   * background makes at the top of the screen.
   *
   * The ellipse is centred at 62% because that is roughly where the
   * button lands, not at 50%: the glow should be behind the thing you
   * are meant to press, not behind the middle of the box. */
  --dp-break-bg:
    radial-gradient(ellipse 120% 90% at 50% 62%, rgba(157, 78, 221, .30), transparent 70%),
    linear-gradient(180deg, rgba(119, 0, 255, .04) 0%, rgba(119, 0, 255, .13) 50%, rgba(119, 0, 255, .04) 100%);
  /* A hairline at each edge so the band still has a boundary once the
   * fill fades out to nothing there. The light theme wants no such line
   * — its band is a solid block and an edge on top of one is a seam. */
  --dp-break-edge: rgba(199, 125, 255, .18);
}

body.dp-light,
body.dp-dark {
  margin: 0;
  padding: 0;
  color: var(--dp-ink);
  font-family: 'Outfit', system-ui, -apple-system, 'Segoe UI', sans-serif;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

body.dp-light { background: var(--dp-surface); }

/* THE canonical DreamPark backdrop, character for character the stack in
 * views/share/share-head.ejs and views/download.ejs: one violet ellipse
 * at the top, a pink and a cyan wash further down at low opacity, over
 * the ink-void → ink-deep vertical gradient. It is copied rather than
 * shared because those two are inline <style> blocks in their own views
 * and there is no stylesheet either of them loads; if that ever changes,
 * this is the fourth caller to point at it.
 *
 * NO background-attachment: fixed. iOS Safari does not support it and can
 * drop the gradient entirely — share-head gets away with it, /download
 * deliberately does not, and this page is the one with 75vh of video at
 * the top where a missing background is most obvious.
 *
 * background-color underneath is the solid base for the area a gradient
 * cannot reach, and matches <meta name="theme-color">. */
body.dp-dark {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(106, 27, 154, .45), transparent 55%),
    radial-gradient(ellipse at 80% 50%, rgba(255, 62, 181, .14), transparent 60%),
    radial-gradient(ellipse at 20% 80%, rgba(0, 229, 255, .10), transparent 55%),
    linear-gradient(180deg, var(--mdp-ui-ink-void, #0c0220) 0%, var(--mdp-ui-ink-deep, #1a0633) 100%);
  background-color: var(--mdp-ui-ink-void, #0c0220);
}

/* The root, for Safari's chrome and overscroll — see the file header. */
html.dp-light { background: #ffffff; }
html.dp-dark  { background: var(--mdp-ui-ink-void, #0c0220); }

/* Text selection. The gold-on-ink pair is the site's, from share-head and
 * /download; on white it would be unreadable, so the light theme keeps
 * the UA default and this is scoped rather than global. */
body.dp-dark ::selection { background: var(--mdp-ui-gold, #ffd166); color: #2a1a00; }

/* Present for screen readers and crawlers, absent for eyes. On a page
 * whose entire visible content is imagery, this is how the <h1> exists. */
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Header ────────────────────────────────────────────────────────────
 * Moved to views/partials/site-header.ejs + public/css/site-header.css.
 * The .dp-header class is still emitted (site-header's legacyClass) so
 * public/js/dp-home.js's scroll toggle and anything else keyed to it
 * keep working; the chrome itself is the shared header's now.
 *
 * One deliberate difference: the bottom hairline used to appear only
 * past 8px of scroll. It is always present now, as it is on every other
 * surface. --dp-header-h is gone too — use --site-header-h, which the
 * partial publishes with its real rendered height.
 */

/* THE BAR IS GLASS. site-header.css paints a frosted-glass fill on
 * .dp-header so the page (dark home, light download, portals) shows
 * through. Do not re-tint it purple here — a purple --site-header-bg
 * is the slab the public header was asked to drop. */

/* ── The brand: wordmark, not the mark ─────────────────────────────────
 * home.ejs passes brandMark:'responsive', which ships BOTH assets and
 * lets CSS choose. site-header.css's own choice is made at 640px and at
 * a 28px cap height, sized for the developer/operator portals where the
 * bar holds a surface label and little else. This bar is fuller — four
 * inline nav links, the GET button, the badge — and the wordmark is a
 * 967x75 asset, so at 28px it is ~360px wide. Both numbers move here:
 *
 *   34px, not 28px. "Use the larger logo" was the ask, and at 28px a
 *   13:1 lockup reads as a thin strip rather than as the logo. 34px is
 *   the most the 72px bar takes while keeping the air above and below
 *   the mark comfortable.
 *
 *   900px, not 640px. Between those two widths the wordmark and the nav
 *   are both present and both compressible, and the nav loses — its
 *   links wrap or clip before the brand gives up a pixel. Above 900px
 *   everything fits at full size; below it, the mark. Nothing sits at
 *   an in-between size, which is the state that reads as broken.
 *
 * Scoped to .dp-header (the homepage's legacyClass) so no other surface
 * inherits a homepage-shaped brand. */
/* THE HEIGHT IS A CLAMP, NOT 34px, AND THAT IS WHAT MAKES THE NAV
 * CENTRING POSSIBLE AT ALL. The two asks — a bigger wordmark, and nav
 * links centred in the BAR rather than in the space the brand leaves —
 * fight each other, and the arithmetic decides it:
 *
 *   centring puts the nav at (W - 250) / 2, so the brand may occupy at
 *   most that much minus a gap. At a flat 34px the wordmark is 438px
 *   wide (the asset is 967x75, very nearly 13:1), which needs a 1206px
 *   viewport before a centred nav clears it. Every laptop below that
 *   would have had to drop to the icon mark — which is the one thing
 *   the feedback asked us not to do.
 *
 * So the mark scales with the viewport instead: 34px where there is
 * room for all of it, easing down to 24px on a narrow bar. Nothing ever
 * collides and nothing ever has to disappear. 2.4vw is the slope that
 * keeps `24 + width + 16 <= (W - nav) / 2` true from 900px up — checked
 * at 950/1024/1100/1280/1440.
 *
 * The upper bound is deliberately the old 34px rather than something
 * larger: 34px in a 72px bar already leaves the mark and the nav on the
 * same optical line, and going past it makes the bar look packed. */
.dp-header .dph-mark-text { height: clamp(24px, 2.4vw, 34px); }

/* ── The nav sits in the MIDDLE OF THE BAR ─────────────────────────────
 * site-header.css lays the bar out with `justify-content: space-between`
 * and three children, so the nav's position is whatever the brand and
 * the actions leave it — which was fine with a 28px icon mark and reads
 * as visibly off-centre once the brand is a wordmark. Taking the nav out
 * of flow centres it on the bar itself, and leaves the remaining two
 * children to space-between exactly as before.
 *
 * Above 900px only, for two reasons: it is where the wordmark gives way
 * to the icon mark anyway, and below it the phone bar carries FOUR
 * inline links (navInline) with no spare width — an absolutely centred
 * nav there would sit on top of the brand rather than beside it. */
@media (min-width: 901px) {
  .dp-header .dph-bar { position: relative; }
  .dp-header .dph-nav {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    margin: 0;
  }
}

@media (max-width: 900px) {
  /* Below 900 the homepage runs the icon mark where site-header.css
   * would still be running the wordmark (its own swap is at 640). Both
   * declarations are needed: the partial's rule hides the icon for every
   * 'responsive' brand above 640px, so re-showing it is not implied by
   * hiding the wordmark. */
  .dp-header .dph-brand-responsive .dph-mark-text { display: none; }
  .dp-header .dph-brand-responsive .dph-mark-icon { display: block; height: 30px; }
}

/* ── The violet CTA ────────────────────────────────────────────────────
 * The .dl-mega recipe from views/download.ejs, shrunk for the header.
 * The base glow below is the TIGHT one, because a 48px violet bloom that
 * reads as premium on #0a0617 reads as a smudge on #fff — and the dark
 * theme, which can carry the full bloom, opens it back up in its own
 * block at the end of this section. Base = the constrained case, theme
 * override = the permissive one, so the white page cannot inherit a
 * smudge by accident.
 * Keyframes are dp*-prefixed so they can never collide with the
 * download page's dl* ones if both stylesheets ever meet. */
/* THE violet button — the same object as .dl-mega on /download, not a
 * lookalike: same three gradient stops, same white specular overlay, same
 * breathe + glisten, same uppercase 800 lettering. Only the size and the
 * glow radius differ, because this one sits in a 72px header on white
 * rather than full-size on near-black. */
.dp-cta {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 44px;
  padding: 0 26px;
  border: 1px solid var(--dp-violet-edge);
  border-radius: 999px;
  cursor: pointer;
  text-decoration: none;
  color: #fff;
  font-family: var(--mdp-ui-heading, 'Outfit', system-ui, sans-serif);
  font-size: 13px;
  font-weight: 800;
  line-height: 1;
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
  isolation: isolate;
  overflow: hidden;
  background: var(--dp-violet-fill);
  box-shadow:
    0 10px 32px rgba(119, 0, 255, .38),
    inset 0 1px 0 rgba(255, 255, 255, .35);
  animation: dpBreathe 2.8s ease-in-out infinite;
  transition: transform .18s var(--dp-ease), box-shadow .18s ease, filter .18s ease;
}
.dp-cta::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, .55) 50%, transparent 62%);
  transform: translateX(-120%);
  animation: dpGlisten 4.4s ease-in-out infinite;
  pointer-events: none;
}
.dp-cta:hover {
  transform: translateY(-2px) scale(1.02);
  filter: brightness(1.08);
  box-shadow: 0 16px 44px rgba(119, 0, 255, .5), 0 0 26px rgba(199, 125, 255, .38),
              inset 0 1px 0 rgba(255, 255, 255, .45);
}
.dp-cta:active { transform: translateY(0) scale(.975); }
.dp-cta--lg { height: 58px; padding: 0 44px; font-size: 16px; }

.dp-cta:focus-visible { outline: 3px solid var(--dp-violet); outline-offset: 3px; }

/* On the dark page, the full .dl-mega bloom — the reason the glow was
 * ever tightened was the white background, and it is gone. The focus
 * ring moves up to --dp-violet-3 for the same reason the panel's close
 * button uses it: #7700ff on #0c0220 is a ring you have to look for. */
.dp-dark .dp-cta {
  box-shadow:
    0 12px 40px rgba(119, 0, 255, .55),
    0 0 22px rgba(199, 125, 255, .28),
    inset 0 1px 0 rgba(255, 255, 255, .35);
}
.dp-dark .dp-cta:hover {
  box-shadow:
    0 18px 52px rgba(119, 0, 255, .65),
    0 0 34px rgba(199, 125, 255, .45),
    inset 0 1px 0 rgba(255, 255, 255, .45);
}
.dp-dark .dp-cta:focus-visible { outline-color: var(--dp-violet-3); }

/* ── The Discord button, purple on the dark page ───────────────────────
 * It is a GHOST button by default — a hairline and a whisper of fill,
 * site-header's .dph-cta-ghost — which is right on the white page, where
 * the band is a pale panel and a solid violet slab in the middle of it
 * would be the loudest thing on a page that is otherwise all artwork.
 * On the dark page the same button is a faint outline on a dark violet
 * wash, i.e. the one call to action in the feed reads as disabled. So on
 * dark it becomes the real button.
 *
 * PAINT ONLY — no height, no padding, no font-size. .dp-break-cta's own
 * sizing lives in dp-attraction-feed.css at single-class specificity,
 * and this sheet loads after it, so anything geometric written here
 * would win and shrink the hero button back to a 44px header pill. That
 * is also why this cannot simply be added to the .dp-cta selector list,
 * which is the obvious way to share the recipe and is a trap.
 *
 * The values come from --dp-violet-fill / --dp-violet-edge, so the two
 * buttons cannot drift; only the glow is stated here, at the reel-tile
 * scale rather than the header's. */
.dp-dark .dp-break-cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  color: #fff;
  border: 1px solid var(--dp-violet-edge);
  background: var(--dp-violet-fill);
  box-shadow:
    0 14px 46px rgba(119, 0, 255, .5),
    0 0 26px rgba(199, 125, 255, .3),
    inset 0 1px 0 rgba(255, 255, 255, .35);
  transition: transform .18s var(--dp-ease), box-shadow .18s ease, filter .18s ease;
}
/* The same specular sweep the GET pill runs, on the same keyframes, so
 * the two really are one object at two sizes. */
.dp-dark .dp-break-cta::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(105deg, transparent 38%, rgba(255, 255, 255, .5) 50%, transparent 62%);
  transform: translateX(-120%);
  animation: dpGlisten 4.4s ease-in-out infinite;
  pointer-events: none;
}
.dp-dark .dp-break-cta:hover {
  /* NO BLURPLE, AND `color` IS THE HALF OF THAT WHICH IS EASY TO MISS.
   * site-header.css's .dph-cta-discord-ghost:hover sets three things —
   * border-color, background and `color: #4048d8` — because a ghost
   * button has no colour of its own and Discord's brand arriving on
   * hover is the whole point of that variant. This button is already
   * violet, so all three have to be answered here or the label alone
   * turns blurple on a violet fill, which is what shipped and what it
   * looked like: the text going purple on hover.
   *
   * The two rules are the same specificity (0,2,0) and this sheet loads
   * after site-header.css, so order is what wins it — every property
   * that rule sets must be restated, not just the ones that looked
   * wrong first. */
  color: #fff;
  transform: translateY(-2px) scale(1.02);
  filter: brightness(1.08);
  background: var(--dp-violet-fill);
  border-color: var(--dp-violet-3);
  box-shadow:
    0 20px 58px rgba(119, 0, 255, .6),
    0 0 38px rgba(199, 125, 255, .45),
    inset 0 1px 0 rgba(255, 255, 255, .45);
}
.dp-dark .dp-break-cta:active { transform: translateY(0) scale(.985); }
.dp-dark .dp-break-cta:focus-visible { outline-color: var(--dp-violet-3); }
/* The mark is Discord's own and stays white on the violet. */
.dp-dark .dp-break-cta svg { color: #fff; }

/* ── The button, on a phone ────────────────────────────────────────────
 * It goes. Below 760px the header's nav stays inline (navInline in
 * views/home.ejs) and its third item is the word "app" — the same
 * /download link this button is. Keeping both would put the same
 * destination in the bar twice, and a violet pill next to three plain
 * words is the loudest thing on a screen that is mostly video.
 *
 * 760px is the site-header breakpoint, not a number picked here: above
 * it the nav is a desktop row and the button is the page's call to
 * action; below it the nav IS the row of links and the button is a
 * duplicate. The old + mark left with it — GET is one word, and a mark
 * sized to a 13px cap height was drawn for a two-word label. */
@media (max-width: 760px) {
  .dp-header .dp-cta { display: none; }
}

@keyframes dpBreathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.022); } }
@keyframes dpGlisten { 0%, 62% { transform: translateX(-120%); } 86%, 100% { transform: translateX(120%); } }

/* ── Hero reel ─────────────────────────────────────────────────────────
 * A height-driven row that derives a WIDTH for its cards, because the
 * card is a width-sized container-query component (see
 * dp-attraction-card.css) and sizing it by height would break every cqw
 * inside it. */
/* Flush to the header. The reel is the hero — a gap above it reads as a
 * page that has not finished loading. */
.dp-reel-wrap { padding: 0; }

.dp-reel {
  /* The reel IS the hero, so it takes hero space: roughly three quarters
   * of the viewport. Anything less and it reads as another row of the
   * grid rather than the thing the page opens with. */
  --reel-h: clamp(420px, 75vh, 900px);
  /* Reserve the row. Without this the reel is 0px tall until its cards
   * paint and everything below jumps — measured as a single 0.05 layout
   * shift, which was the page's entire CLS. Also covers an empty reel. */
  min-height: var(--reel-h);
  display: flex;
  gap: clamp(8px, 1.4vw, 18px);
  padding: 0 var(--dp-gutter) 6px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  /* NO scroll-snap here. The autoscroll advances scrollLeft by a fraction
   * of a pixel per frame and `scroll-snap-type: x mandatory` snapped every
   * one of those back to the nearest tile — the reel could not move at
   * all. Snap is correct for the reduced-motion case below, where the
   * reel is a manual swipe strip, and only there. */
}
.dp-reel::-webkit-scrollbar { display: none; }

/* The row height IS the tile height: a card is media and nothing else. */
.dp-reel .dpc {
  flex: 0 0 auto;
  width: calc(var(--reel-h) * 9 / 16);
}
.dp-reel .dpc-media { height: var(--reel-h); aspect-ratio: auto; }
/* Nothing is drawn over a hero tile, so the scrim that exists to make the
 * grid card's preview rect readable has no job here. */
.dp-reel .dpc-media::after { display: none; }

/* ── Hot / Latest, the grid, the load-more affordance ──────────────────
 * MOVED to public/css/dp-attraction-feed.css (Aug 2026), unchanged, so
 * the creator share page can mount the same grid. home.ejs loads both
 * sheets; the --dp-* tokens under .dp-light / .dp-dark above still feed
 * it, which is why it renders correctly in either theme without a single
 * theme-aware rule of its own. Change the grid there, not here, or the
 * two surfaces drift — which is the whole reason it moved. */


/* ── The full-screen attraction view ───────────────────────────────────
 * MOVED to public/css/dp-attraction-panel.css (Aug 2026), unchanged, so a
 * page that opens the panel can load its rules without loading this whole
 * homepage sheet. home.ejs loads both. The panel was always dark — it is
 * the one part of this page that needed no work when the default flipped,
 * and on the dark page it now reads as the page zooming in rather than as
 * a dark sheet arriving over a white one. */

@media (prefers-reduced-motion: reduce) {
  /* Nothing moves on its own, so the reel becomes an ordinary swipe strip
   * — and THAT is where snapping belongs. */
  .dp-reel { scroll-snap-type: x mandatory; }
  .dp-reel .dpc { scroll-snap-align: center; }

  .dp-cta, .dp-cta::after,
  .dp-dark .dp-break-cta, .dp-dark .dp-break-cta::after { animation: none; }
  /* .dp-tab/.dp-tabs-underline are repeated in dp-attraction-feed.css and
   * .dp-panel/.dp-scrim in dp-attraction-panel.css, so a page loading either
   * component without this sheet still honours the preference. */
  .dp-cta, .dp-tab, .dp-tabs-underline, .dp-panel, .dp-scrim { transition: none; }
}
