/* ════════════════════════════════════════════════════════════════════════
 * dp-home.css — the white, media-first homepage.
 *
 * 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, white page or dark panel.
 *
 * THE LIGHT THEME, AND WHAT IT DOES NOT TOUCH
 * ───────────────────────────────────────────
 * public/css/mydreampark-ui.css is dark-only and this page still 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.
 *
 * A dark modal over a dimmed white page is a correct, deliberate look
 * (the modal already sits on a rgba(0,0,0,.72) scrim), so the coexistence
 * costs nothing. Our own tokens live under .dp-light on <body> and are
 * namespaced --dp-*.
 *
 * 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.
 * ════════════════════════════════════════════════════════════════════ */

.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 brand violet, straight from .dl-mega on /download. Same three
   * stops so the two pages' CTAs are the same object. */
  --dp-violet: #7700ff;
  --dp-violet-2: #9d4edd;
  --dp-violet-3: #c77dff;

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

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

/* 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 violet CTA ────────────────────────────────────────────────────
 * The .dl-mega recipe from views/download.ejs, shrunk for the header and
 * with the glow tightened for a white background (a 48px violet bloom
 * that reads as premium on #0a0617 reads as a smudge on #fff).
 * 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 rgba(199, 125, 255, .55);
  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:
    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%);
  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; }

/* ── The add mark ──────────────────────────────────────────────────────
 * Sized to the cap height of the 13px/800 label rather than to the
 * button, and stroked at 2.6 in a 24 viewBox — about 1.6px as rendered,
 * which is the weight of the lettering's stems. A hairline + next to
 * 800-weight uppercase reads as a different object; this reads as part
 * of the same word.
 *
 * The left padding comes in 6px because a + is optically lighter than a
 * capital: at an even 26px both sides the button sat visibly left-heavy. */
.dp-cta-icon {
  flex: 0 0 auto;
  display: block;
  width: 15px;
  height: 15px;
}
.dp-cta--icon { padding-left: 20px; }
.dp-cta--lg.dp-cta--icon { padding-left: 36px; }
.dp-cta--lg .dp-cta-icon { width: 18px; height: 18px; }
.dp-cta:focus-visible { outline: 3px solid var(--dp-violet); outline-offset: 3px; }

@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 unselected tab is faded rather than greyed so it still reads as a
 * word you can press. The underline is one element that slides — see
 * dp-home.js; it is measured from the active tab, never hard-coded. */
.dp-tabs {
  position: relative;
  display: flex;
  gap: clamp(18px, 3vw, 30px);
  align-items: flex-end;
  padding: clamp(20px, 3.4vw, 34px) var(--dp-gutter) 0;
}
.dp-tab {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  padding: 0 0 10px;
  margin: 0;
  cursor: pointer;
  color: inherit;
  font: 800 clamp(20px, 3.4vw, 30px)/1 'Outfit', system-ui, sans-serif;
  letter-spacing: -.025em;
  opacity: .42;
  transition: opacity .22s ease;
}
.dp-tab[aria-selected="true"] { opacity: 1; }
.dp-tab:focus-visible { outline: 3px solid var(--dp-violet); outline-offset: 4px; border-radius: 4px; }
.dp-tabs-underline {
  position: absolute;
  bottom: 0;
  /* left:0, NOT the gutter. dp-home.js positions this purely with
   * translateX measured from the tabs container's own box, so any CSS
   * inset here is added on top and pushes the bar one gutter too far. */
  left: 0;
  width: 0;
  height: 3px;
  border-radius: 2px;
  background: currentColor;
  transform: translateX(0);
  transition: transform .24s var(--dp-ease), width .24s var(--dp-ease);
  pointer-events: none;
}

/* ── The grid ──────────────────────────────────────────────────────────
 * auto-fill + a clamped minimum, so the column count is a consequence of
 * the viewport rather than a set of breakpoints to maintain. */
.dp-grid {
  display: grid;
  gap: clamp(10px, 2vw, 20px);
  grid-template-columns: repeat(auto-fill, minmax(clamp(148px, 21vw, 240px), 1fr));
  padding: clamp(14px, 2.4vw, 24px) var(--dp-gutter) 0;
}

/* content-visibility keeps a several-hundred-card grid at 60fps. Two
 * things about it are easy to get wrong and both were measured:
 *
 *  · It must not apply to the cards that are on screen at first paint.
 *    Un-rendering an off-screen subtree of a card the browser has
 *    already laid out is recorded as a layout shift — 0.25 CLS from the
 *    one line meant to make scrolling smoother.
 *
 *  · contain-intrinsic-size has to be RIGHT. The column width is
 *    auto-filled, so no fixed px guess can be; dp-home.js keeps
 *    --dp-card-h exact from a real measurement (the card is now simply
 *    its 9:16 media) and re-measures on resize.
 *
 * POSITION, not provenance. This used to be a `dpc--virt` class that
 * dp-home.js added to client-appended cards only — which meant Hot
 * (server-rendered first page) and Latest (fetched and appended) were
 * literally two differently-styled grids of the same card. They are one
 * grid now: the rule keys off nth-child, so card 13 onwards virtualises
 * whether it arrived in the HTML or over the wire, and cards 1-12 never
 * do. Twelve covers the first screenful at every column count from 2-up
 * (six rows) to 9-up (one and a bit). */
.dp-grid > .dpc:nth-child(n + 13) {
  content-visibility: auto;
  contain-intrinsic-size: auto var(--dp-card-h, 430px);
}

/* The infinite-scroll trigger. Deliberately paints nothing: an element
 * that renders no pixels can be moved down the page by an append without
 * the browser recording a layout shift, which a visible "Show more"
 * button in the same position cannot. */
.dp-sentinel { height: 1px; margin: 0; padding: 0; pointer-events: none; }

/* Placeholder for a page still in flight. Must match a real card's height
 * EXACTLY — which is now simply its 9:16 media — or reserving the space
 * defeats its own purpose. */
.dpc--skel { pointer-events: none; }
.dpc--skel .dpc-media { background: var(--dp-surface-2); }

.dp-more {
  display: flex;
  justify-content: center;
  padding: clamp(24px, 4vw, 44px) var(--dp-gutter) clamp(48px, 8vw, 96px);
}
.dp-more[hidden] { display: none; }
.dp-more-btn {
  appearance: none;
  border: 1px solid var(--dp-line);
  background: var(--dp-surface);
  color: var(--dp-ink);
  border-radius: 999px;
  padding: 12px 26px;
  cursor: pointer;
  font: 700 13.5px/1 'Outfit', system-ui, sans-serif;
  transition: border-color .18s ease, background .18s ease;
}
.dp-more-btn:hover { border-color: var(--dp-violet); background: var(--dp-surface-2); }
.dp-more-btn:disabled { opacity: .45; cursor: default; }
.dp-more-btn:focus-visible { outline: 3px solid var(--dp-violet); outline-offset: 3px; }


/* ── The full-screen attraction view ───────────────────────────────────
 * Tapping a card opens the attraction over the whole screen, with one X
 * in the top-left.
 *
 * WHAT IS INSIDE IT IS THE SHARE PAGE'S OWN MARKUP — literally
 * views/share/share-attraction-main.ejs, fetched as an HTML fragment from
 * /share/attraction/{cid}/{slug}/fragment and injected. So there are NO
 * styles here for the CONTENT of the panel, and there should never be
 * any: it is styled by views/share/share-styles.ejs, the same partial the
 * share page uses, which views/home.ejs now includes. Everything below is
 * chrome — the scrim, the surface, the close button, the scroller.
 *
 * This replaced an <iframe> of /share/attraction/**\/embed. The isolation
 * an iframe gave was real, but it cost a whole HTML document per open and
 * its navigations pushed entries into the top-level history, which is
 * what left the address bar on the attraction after the panel closed.
 * Sharing the stylesheet gets the same one-definition guarantee without
 * either cost.
 *
 * It stays dark on a white page, because the share markup is dark: the
 * --sh-* ink set is tuned for a dark backdrop (share-styles.ejs) and a
 * white surround under it would be the only wrong thing on the screen.
 * ──────────────────────────────────────────────────────────────────── */
/* ABOVE THE SITE HEADER. The header is position:fixed at
 * z-index: var(--site-header-z, 1000) (public/css/site-header.css), so a
 * panel at 90 opened UNDER it — the close button in the top-left corner
 * was the part it covered, which is the one control the overlay has.
 *
 * Derived from the header's own token rather than hard-coded above it, so
 * raising --site-header-z somewhere cannot quietly put the header back on
 * top. Still below the shared modals at 2000 (mydreampark-ui.css) —
 * DreamShareModal opens FROM this panel and has to land over it.
 *
 * The header is left visible on purpose. It is inert while the panel is
 * open (dp-attraction-panel.js), the panel is opaque, and hiding a
 * position:fixed header buys nothing but a flicker on close. */
.dp-scrim {
  position: fixed;
  inset: 0;
  z-index: calc(var(--site-header-z, 1000) + 10);
  background: rgba(8, 4, 20, .5);
  backdrop-filter: blur(16px) saturate(.9);
  -webkit-backdrop-filter: blur(16px) saturate(.9);
  opacity: 0;
  pointer-events: none;
  transition: opacity .26s ease;
}
.dp-scrim.is-open { opacity: 1; pointer-events: auto; }

.dp-panel {
  position: fixed;
  inset: 0;
  z-index: calc(var(--site-header-z, 1000) + 11);
  /* The share page's own backdrop, so injected markup sits on what it was
   * designed for. Kept in sync with the body rule in share-head.ejs. */
  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%);
  color: var(--sh-ink-body);
  font-family: var(--mdp-ui-body);
  opacity: 0;
  transform: translateY(10px);
  transition: opacity .2s ease, transform .26s var(--dp-ease);
  will-change: transform, opacity;
}
.dp-panel[hidden] { display: none; }
.dp-panel.is-open { opacity: 1; transform: none; }

/* The close button floats over the content, so it gets a little dark at
 * its back — only at the very top, and only a gradient. */
.dp-panel::before {
  content: '';
  position: absolute;
  z-index: 1;
  inset: 0 0 auto 0;
  height: 96px;
  pointer-events: none;
  background: linear-gradient(rgba(4, 2, 10, .55), transparent);
}

/* TOP-LEFT, and outside the scroller so it holds its corner while the
 * attraction scrolls under it. */
.dp-panel-close {
  position: absolute;
  z-index: 2;
  top: max(clamp(10px, 1.6vw, 20px), env(safe-area-inset-top));
  left: max(clamp(10px, 1.6vw, 20px), env(safe-area-inset-left));
  width: 42px;
  height: 42px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, .16);
  background: rgba(10, 6, 23, .62);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  color: #f4f2ff;
  border-radius: 50%;
  cursor: pointer;
  transition: background .16s ease, transform .16s ease;
}
.dp-panel-close:hover { background: rgba(28, 18, 54, .92); transform: scale(1.06); }
.dp-panel-close:focus-visible { outline: 3px solid var(--dp-violet-3); outline-offset: 2px; }

/* The scroller carries .sh-embed and its child carries .sh-wrap, which is
 * exactly the pair a chrome-less share page uses — so the measure and the
 * padding come from share-styles.ejs rather than being restated here. */
.dp-panel-scroll {
  height: 100%;
  height: 100svh;
  overflow-y: auto;
  overflow-x: hidden;
  /* The page behind is position:fixed while this is open, but a rubber-band
   * at the end of this scroller still chains to it on iOS without this. */
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
}
/* Clear the floating X. On the share page the site header does this job. */
.dp-panel-scroll .sh-wrap { padding-top: clamp(64px, 8vh, 88px); }

/* While the fragment is in flight. Not a spinner: the surface is already
 * on screen and animating in, and a spinner on top of that reads as two
 * things loading. */
.dp-panel-body.is-loading {
  min-height: 60vh;
  background: linear-gradient(100deg, rgba(255, 255, 255, .03) 20%,
              rgba(255, 255, 255, .07) 40%, rgba(255, 255, 255, .03) 60%);
  background-size: 220% 100%;
  border-radius: 22px;
  animation: dpcShimmer 1.7s linear infinite;
}


@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 { animation: none; }
  .dp-cta, .dp-tab, .dp-tabs-underline, .dp-panel, .dp-scrim { transition: none; }
}
