/* ════════════════════════════════════════════════════════════════════════
 * dp-attraction-feed.css — the Hot/Latest attraction grid, as a component.
 *
 * Lifted verbatim out of dp-home.css so a second page can mount the same
 * grid instead of growing a second copy of it. The rules below are
 * unchanged; the only addition is the token block directly underneath,
 * which is what makes the component work off the white homepage.
 *
 * THE CARD ITSELF IS NOT HERE. public/css/dp-attraction-card.css owns it
 * and is deliberately independent of everything in this file — same card,
 * white page or dark share page. This file owns only the furniture
 * AROUND the cards: the sort tabs, the grid box, the load-more affordance.
 *
 * THEMING IS ONE OPT-IN MODIFIER, AND THE DEFAULT IS "INHERIT".
 * ──────────────────────────────────────────────────────────
 * Every rule below already read --dp-* custom properties that the
 * homepage defines under .dp-light on <body>. `.dp-feed` therefore
 * declares NOTHING: it inherits whatever the host page set, so the white
 * homepage resolves exactly the values it always did.
 *
 * That is not a stylistic preference, it is the bug this shape avoids.
 * The first version put dark defaults on `.dp-feed` "for a dark host,
 * with .dp-light overriding them" — which is backwards. `.dp-light` is on
 * <body> and `.dp-feed` is its DESCENDANT, so the descendant wins for its
 * whole subtree, and the homepage's Show more button came out white on
 * white. A host that actually needs the dark set opts in with
 * `.dp-feed--dark` (views/partials/attraction-feed.ejs: `feedDark`).
 *
 * --dp-gutter is the other one: the homepage runs edge to edge and pays
 * for its own side padding here, while a share page is already inside
 * .sh-wrap's 20px column and must not pay twice. Hence 0 on
 * .dp-feed--flush rather than a negative margin, which is what the first
 * attempt did and it broke the grid's auto-fill maths at the ends.
 * ════════════════════════════════════════════════════════════════════ */

/* A dark host — any /share page. Mapped onto the --sh-* set so the grid
 * picks up that page's own theme colour rather than a second violet. */
.dp-feed--dark {
  --dp-ink: var(--sh-ink, #fff);
  --dp-ink-soft: var(--sh-dim, rgba(235, 228, 245, .62));
  --dp-surface: var(--sh-surface, rgba(255, 255, 255, .03));
  --dp-surface-2: var(--sh-surface-2, rgba(255, 255, 255, .06));
  --dp-line: var(--sh-line, rgba(255, 255, 255, .12));
  --dp-violet: var(--sh-theme, #9d4edd);
  --dp-ease: cubic-bezier(.16, 1, .3, 1);
  /* Only a fallback. A dark host that is ALSO --flush gets 0 from the rule
   * below, which is later in this file and therefore wins at equal
   * specificity. Without a value here the `padding: X var(--dp-gutter) 0`
   * shorthand would be invalid and drop entirely. */
  --dp-gutter: clamp(14px, 3vw, 32px);
}

/* Inside a column that already has side padding (any /share page's
 * .sh-wrap), the grid must not add its own or the cards inset twice and
 * stop lining up with the section headings above them. */
.dp-feed--flush { --dp-gutter: 0px; }

/* ── 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;
  /* --dp-ink, NOT inherit. On the white homepage the two are the same
     value, so nothing there changes. On a dark share page `inherit` picked
     up the body's --sh-ink-body, which is white at 78% — so the SELECTED
     tab, the one at full opacity, still read as a muted grey. The ink token
     is the page's actual foreground and resolves to #fff there. */
  color: var(--dp-ink);
  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;
  /* TWO gaps, not one, and the row gap is the bigger of them. The preview
   * rect hangs below its clip now (.dpc-preview in dp-attraction-card.css)
   * and it needs somewhere to hang INTO — at the old uniform gap the
   * overhanging rect of one row nearly touched the clip of the next, which
   * is the thing that made the corner placement look safer. Columns stay
   * tighter because nothing overhangs sideways. */
  column-gap: clamp(10px, 2vw, 20px);
  row-gap: clamp(26px, 4vw, 46px);
  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 Discord break ─────────────────────────────────────────────────
 * The one thing in the grid that is not a card. Opt-in per feed
 * (`feedBreak` in attraction-feed.ejs); today only the homepage asks.
 *
 * IT IS A GRID ITEM (grid-column: 1 / -1), not a section wedged between
 * two grids. Two grids would auto-fill their column counts independently
 * at some widths and the cards above and below would stop lining up,
 * which is the one thing a break across a grid must not do.
 *
 * IT IS A TONAL PANEL, WITH NO RULE ON IT. An earlier cut drew a hairline
 * top and bottom; without them there was nothing doing the separating at
 * all — white type and a white-glass button on a white page, in a gap the
 * grid could plausibly have left by itself. A band of the page's own
 * off-white separates without drawing a line anywhere: the eye reads a
 * change of surface faster than it reads 1px, and it stays as quiet as
 * the rest of the page.
 *
 * It sits at DOM position 13+, which is where the virtualisation rule
 * above starts — the band pushes card 13 to :nth-child(14), so cards 1-12
 * still never virtualise and everything from 13 on still does.
 *
 * Position is maintained by placeBreak() in dp-attraction-feed.js; see
 * the note in partials/discord-break.ejs for why static markup is not
 * enough on its own. */
.dp-break {
  grid-column: 1 / -1;
  /* Full bleed. The grid pads itself by one gutter each side, and a panel
   * that stopped short of the screen edge would read as a very wide card
   * rather than as the page changing underfoot. (Inside .dp-feed--flush
   * the gutter is 0 and this correctly becomes a no-op.) */
  margin-left: calc(var(--dp-gutter) * -1);
  margin-right: calc(var(--dp-gutter) * -1);
  background: var(--dp-surface-2);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(22px, 3.2vw, 34px);
  padding: clamp(64px, 12vw, 148px) var(--dp-gutter);
  text-align: center;

  /* THE INK TOKENS THE GHOST BUTTON READS, pinned rather than inherited.
     .dph-cta-ghost is written against --mdp-*, which is how it flips with
     a header's theme for free — and those values come from
     body.mdp-chrome-dark, whose CSS ships inside the BADGE partial.
     The homepage passes badgeSignedOut:false, so a signed-out visitor
     renders no badge, no badge stylesheet, and the fallbacks are white:
     the button would be white-on-white for exactly the people most worth
     inviting. So the band states the values it needs. They are
     body.mdp-chrome-dark's own, verbatim, scoped to .dp-break so nothing
     else on the page — least of all the badge or the auth modal — sees
     them. */
  --mdp-text: #150a24;
  --mdp-border: rgba(21, 10, 36, .18);
  --mdp-border-hover: rgba(21, 10, 36, .34);
  --mdp-bg: linear-gradient(to bottom, rgba(21, 10, 36, 0), rgba(21, 10, 36, .06));
}

.dp-break-title {
  margin: 0;
  font: 800 clamp(30px, 6.2vw, 64px)/1.02 'Outfit', system-ui, sans-serif;
  letter-spacing: -.035em;
  color: var(--dp-ink);
}

/* THE HEADER'S DISCORD BUTTON, at hero size.
 *
 * The element carries `dph-cta dph-cta-ghost dph-cta-discord-ghost` —
 * site-header's own classes, the same three /download's Discord CTA
 * wears — so the glass fill, the hairline, the radius, the font, the lift
 * and the blurple hover all come from public/css/site-header.css.
 * Everything here is SIZE. Adding a colour, a border or a :hover would
 * fork the two buttons, which is the one thing this arrangement exists to
 * prevent — and it has already happened once, when the blurple hover
 * lived in views/download.ejs and only that page had it.
 *
 * It works because site-header.css is on any page with a header and this
 * sheet is linked after it, so these single-class rules win over its
 * single-class ones — including the 720px rule that tightens .dph-cta's
 * padding for a crowded bar, which has no business applying to a button
 * standing alone in half a screen of white.
 *
 * No dph-cta-has-icon, deliberately: that class is what drops the label on
 * a narrow header. Here the words are the point. */
.dp-break-cta {
  height: clamp(54px, 6.6vw, 68px);
  padding: 0 clamp(26px, 3.8vw, 40px);
  gap: clamp(10px, 1.3vw, 14px);
  font-size: clamp(0.92rem, 1.5vw, 1.08rem);
}
.dp-break-cta svg { width: clamp(22px, 2.7vw, 28px); height: auto; }
/* The one thing NOT inherited, because there is nothing to inherit:
   site-header.css ships no focus rule, so its buttons fall back to the UA
   ring. Every other control in this component uses this one, and a focus
   ring is invisible to a mouse — it is not part of the button's look. */
.dp-break-cta:focus-visible { outline: 3px solid var(--dp-violet); outline-offset: 4px; }

/* 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; }

/* Only ever drawn when a host page passes `feedEmpty`. The homepage does
 * not: an empty catalogue there is an outage, not a state worth wording.
 * A creator with no published attractions yet is a real, ordinary state. */
.dp-feed-empty {
  margin: 0;
  padding: clamp(28px, 6vw, 56px) var(--dp-gutter);
  text-align: center;
  color: var(--dp-ink-soft);
  font: 500 15px/1.6 'Outfit', system-ui, sans-serif;
}

/* 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; }

@media (prefers-reduced-motion: reduce) {
  /* dp-home.css kills the same two transitions for the homepage; repeated
   * here because a share page mounting this component does not load that
   * sheet, and a sliding underline is exactly the kind of motion this
   * preference is about. */
  .dp-tab, .dp-tabs-underline, .dp-break-cta { transition: none; }
}
