/* ==========================================================================
   DOLL HAUS — pages.css
   Page-specific overrides only. Anything reusable belongs in components.css.

   Populated in Phase 3 (homepage) and Phase 4 (collection, property and
   product templates). Loaded by every page from Phase 2 onward so the link
   tag never has to change.
   ========================================================================== */


/* --------------------------------------------------------------------------
   BREADCRUMB — collection, property and product pages
   -------------------------------------------------------------------------- */

.breadcrumb {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  /* ONE LINE. A wrapped trail reads as a layout accident, and on a product
     page it pushed the whole gallery down by a row. */
  flex-wrap: nowrap;
  white-space: nowrap;
  overflow: hidden;
  padding-block: var(--s-2) var(--s-3);
  font-family: var(--f-body);
  font-weight: var(--t-label-w);
  font-size: var(--t-label);
  letter-spacing: var(--t-label-ls);
  text-transform: uppercase;
  color: var(--dh-ink-45);
}
/* Ancestors hold their full length; the current page is the one allowed to
   truncate, since the trail above it is the part doing the navigating. */
.breadcrumb > a, .breadcrumb .sep { flex: 0 0 auto; }
.breadcrumb span[aria-current] {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 768px) {
  .breadcrumb { font-size: clamp(8px, 2.6vw, var(--t-label)); letter-spacing: 0.08em; }
}
/* Each crumb is a real link, so each needs a real target — 19px of type is
   not one. The row's own padding comes down to compensate, so the breadcrumb
   occupies the same height it did while every crumb gains 20px of reach. */
.breadcrumb a {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  transition: color var(--dur) var(--ease);
}
.breadcrumb a:hover { color: var(--dh-ink); }
.breadcrumb span[aria-current] { color: var(--dh-ink); }
.breadcrumb .sep { opacity: 0.55; }


/* --------------------------------------------------------------------------
   PRODUCT GRID — collection and property pages
   3-up desktop · 2-up tablet · 1-up mobile
   -------------------------------------------------------------------------- */

.product-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-5);
}
@media (max-width: 1024px) { .product-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 768px)  { .product-grid { grid-template-columns: 1fr; } }


/* ==========================================================================
   HOMEPAGE
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. HERO — full viewport, video slot behind a labelled placeholder
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  /* The promo bar sits above the hero in normal flow and the header overlays
     the hero rather than pushing it down. So the hero takes the viewport
     MINUS the promo bar — first screen = promo + hero = exactly 100svh, with
     nothing clipped and nothing hidden below the fold.
     svh (not vh) so mobile browser chrome doesn't overflow it. */
  height: calc(100svh - var(--anno-h));
  min-height: 460px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--dh-ink);
}

/* The real <video> sits behind the placeholder. When the Wave 1 MP4 arrives
   it drops into the empty <source> and this becomes visible with no rebuild. */
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

/* THE SCRIM (was .hero__slot, an opaque field standing in for the missing
   video — 16 Sep 2026). The footage now plays behind it, so this stops being
   a background and becomes the thing that keeps white Chocogom legible over a
   shot with a brightly lit doll in the middle of it.

   A flat wash would flatten the footage; the gradient is heavier top and
   bottom, where the nav and the CTA sit, and lighter across the middle where
   the doll is. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(to bottom,
      rgba(20, 20, 20, 0.72) 0%,
      rgba(20, 20, 20, 0.38) 34%,
      rgba(20, 20, 20, 0.40) 62%,
      rgba(20, 20, 20, 0.78) 100%);
}

/* NO VIDEO, NO AUTOPLAY, OR REDUCED MOTION: the poster still paints, and the
   scrim still has something to sit on. Also the state a visitor sees on a
   connection slow enough that the first frame has not arrived. */
.hero__video { background-color: #3A3A3A; }

.hero__inner {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* Tightened from --s-6 when the real lockup joined the stack: four items in
     this column instead of three, and the taller stack pushed the CTA under
     the scroll cue. */
  gap: var(--s-5);
  padding-inline: var(--gutter);
  color: var(--dh-invert);
}

/* REVERTED 24 Sep 2026 (Blake): "move the button back to where it was."
   A 21 Sep rule anchored .hero__inner to the bottom of the hero on desktop
   (align-self:stretch + justify-content:flex-end + padding-bottom:237px) to
   hold a constant ~140px gap above the scroll cue once the headline was
   hidden. The headline is back, so the centred column is the composition
   again and the anchor is gone with it. */

/* The positioning line IS the hero headline now — set in Chocogom at
   display size. See the note in index.html: this departs from the spec,
   which reserved the mark for Poppins at label size. */
.hero__headline {
  /* VISIBLE AGAIN, 24 Sep 2026 (Blake). It was hidden with visibility:hidden
     on 21 Sep — box reserved, so the CTA would not move. Both halves of that
     change are now reverted together: the headline shows and the h1 is back
     in the accessibility tree. */

  /* ONE LINE AT EVERY WIDTH (Blake, 25 Sep 2026) — "on one line. no line
     breaks." It used to wrap to three lines at a fixed 96px, held there by
     max-width: 14ch. Both are gone: the size now FOLLOWS the viewport so the
     line always fits instead of the line following the size.

     THE DIVISOR IS MEASURED, NOT GUESSED. "ICONS. OBSESSION. ARTISTRY." sets
     13.47em wide — measured off the RENDERED page (808px of line at a known
     60px font-size), which supersedes the 14.03em first computed from the
     font tables. The table sum ran ~4% high, mostly because it counted the
     Poppins period wider than it is and counted letter-spacing after the
     final character, which the browser paints outside the text box.

     So 14.5 leaves ~7.6% of air, not the ~3% originally intended. Left as
     is: it is slack in the safe direction, it absorbs the desktop scrollbar
     that 100vw counts and the element box does not, and tightening it would
     resize the mobile headline, which nobody has asked for. Tightening to
     13.9 would buy about 4% on a phone if that is ever wanted.

     IF THE WORDS EVER CHANGE, THIS NUMBER IS WRONG. Re-measure the same way:
     set a known font-size, read the rendered line width, divide.

     Sized against the gutter rather than raw 100vw so it keeps fitting if the
     gutter is ever retuned — it is 48px desktop, 24px tablet, 20px mobile. */
  /* BACK TO ONE LINE (client, 1 Oct 2026): "Please revert the hero
     headline back to the single line instead of stacked ... the brand team
     has continually said that's how they want it."

     This restores the 25 Sep composition exactly, declarations and desktop
     hold both, rather than inventing a fourth state. The history:

       to 25 Sep   three lines at a fixed 96px, held by max-width: 14ch
       25 Sep      ONE line at every width, fitted, desktop held at 60px
       30 Sep      three lines, min(96px, fit/5.9), max-width 6.5em,
                   chosen from the dev dock's HEADLINE button
       1 Oct       ONE line again, this rule, at the brand team's insistence

     The three-line measurements (Obsession. 5.467em, the 6.5em window, the
     5.9 divisor) are in git at 8201c14 if it ever comes back. */
  white-space: nowrap;
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: calc((100vw - 2 * var(--gutter)) / 14.5);
  line-height: 1.04;
  letter-spacing: 0.05em;
  color: var(--dh-invert);
}

/* DESKTOP HOLDS AT 60px (Blake, 25 Sep 2026; restored 1 Oct) — "on desktop
   hero headline, reduce the font size to 60px." The fitted calc above would
   put it at 93px at 1440 and 170px at 2560; this stops it filling the width
   on a big monitor and lets the line sit as a quieter band over the footage.

   1025px is the hero's desktop boundary and also where --gutter steps to
   48px, so the tightest case is exactly 1025: content box 929px against a
   line of 60px x 13.47em = 808px. Anything above 1025 only gets roomier,
   because the line is fixed and the window is not.

   THE CEILING IS 68px. Past that the line outgrows the 929px content box at
   1025 and would clip into the gutter. test-home.js computes this rather
   than trusting the comment, so a future bump fails loudly.

   Below 1025px the calc above still fits the line to the window, which is
   what keeps it on one line down to 320px. This is the ONE media query
   allowed to touch this headline's size; test-home.js fails on a second. */
@media (min-width: 1025px) {
  .hero__headline { font-size: 60px; }
}

/* .hero__callout was removed 16 Sep 2026 along with its markup. It named the
   video asset we were waiting for; the asset arrived, so the caption became a
   label announcing the obvious. Its nested duplicate @media block went with
   it — that was a copy-paste that had been overriding itself since 26 Aug. */


/* STATES REVERSED (Blake, 15 Sep 2026). It used to sit as a solid white
   block and hollow out on hover; now it rests as an outline over the hero and
   fills white on hover. The border stays white in both states, so the button's
   footprint never shifts — only the fill and the ink swap. */
/* MATCHED TO THE HEADER'S SUBMIT BUTTON, AND MOVED DOWN TO THE CUE
   (Blake, 29 Sep 2026).

   SIZE. Every value here is lifted from .header__submit rather than picked
   by eye, so the two read as the same control and stay that way if either
   is retuned:

       min-height     36px        was 48
       padding-inline var(--s-3)  was var(--s-4)
       font-size      11px        was 13
       letter-spacing 0.14em      was 0.1em

   It drops under the 44px touch target, which is deliberate and matches the
   header button it is now a twin of. Worth revisiting if tap accuracy on a
   phone is ever raised.

   POSITION. It comes out of .hero__inner's centred column and sits above the
   scroll cue instead. Two consequences worth knowing:

     · the headline column is now the headline alone, so it settles on the
       true vertical centre of the hero rather than being pushed up by the
       button underneath it;
     · the offset is built from the cue's own bottom inset plus the cue's
       height plus a gap, so if --s-5 is retuned the button follows the cue
       instead of drifting away from it. --hero-cue-h is measured from the
       rendered cue (label + gap + icon), and it is the one figure here that
       has to be re-measured if the cue's contents change.

   This is the third position this button has had. It was centred under the
   headline, anchored to the bottom on 21 Sep, reverted to centred on 24 Sep
   ("move the button back to where it was"), and is now anchored again but
   to the cue rather than to the hero floor — which is what makes it hold
   its relationship on a short window instead of drifting. */
/* MEASURED, NOT ESTIMATED. The cue renders 57px tall on desktop (label +
   8px gap + icon) and 53px on a phone. The first pass guessed 44 and the
   button landed 11px off the cue instead of the 24px the offset intends.
   Re-measure this if the cue's contents ever change. */
/* THE HEADER-BACKGROUND ALTERNATE IS GONE (30 Sep 2026).

   JAKKS asked for a solid black bar before scroll with the hero video below
   it rather than behind it; Storm Brain's view was that the transparent bar
   is the better answer, so the transparent bar shipped as the default and the
   solid one sat behind the dock's HEADER button for comparison.

   The dock went with the prototype buttons, so nothing can set data-hdr any
   more and the three rules it drove were dead CSS. Removed rather than left
   as a trap. They are in git at 9625ad6 if the question reopens; what they
   did was give .header--overlay a solid fill at rest, give [data-main] the
   header's height back as padding, and take that same height off the hero so
   the first screen stayed exactly 100svh.

   The transparent bar is what the client was last looking at and is what
   ships. */

.hero { --hero-cue-h: 57px; }

.hero .btn {
  position: absolute;
  left: 50%;
  bottom: calc(var(--s-5) + var(--hero-cue-h) + var(--s-4));
  transform: translateX(-50%);
  z-index: 2;
  white-space: nowrap;

  min-height: 36px;
  padding-inline: var(--s-3);
  font-size: 11px;
  letter-spacing: 0.14em;

  background: transparent;
  color: var(--dh-invert);
  border-color: var(--dh-invert);
}
.hero .btn:hover {
  background: var(--dh-invert);
  color: var(--dh-ink);
  border-color: var(--dh-invert);
}

.hero__cue {
  position: absolute;
  left: 50%;
  bottom: var(--s-5);
  z-index: 2;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-2);
  color: var(--dh-invert);
  opacity: 0.7;
}
.hero__cue .icon { animation: dh-cue 2.4s var(--ease) infinite; }

@keyframes dh-cue {
  0%, 100% { transform: translateY(0); opacity: 0.55; }
  50%      { transform: translateY(6px); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__cue .icon { animation: none; }
}


/* --------------------------------------------------------------------------
   2. PROPERTY CAROUSEL — the centrepiece
   ~2.5 tiles visible desktop, 1.15 mobile. Snap-aligned.
   -------------------------------------------------------------------------- */

.carousel--property .carousel__track {
  grid-auto-columns: calc((100% - (var(--s-5) * 2)) / 2.5);
  gap: var(--s-5);
}

@media (max-width: 1024px) {
  .carousel--property .carousel__track { grid-auto-columns: calc((100% - var(--s-4)) / 1.8); }
}
@media (max-width: 768px) {
  .carousel--property .carousel__track { grid-auto-columns: calc(100% / 1.15); gap: var(--s-4); }
}

/* SHOP ALL doll rails. The tiles are 1:1 with a caption, so more of them
   fit across than the 4:5 property tiles — 3.5 up rather than 2.5. */
.carousel--dolls .carousel__track {
  grid-auto-columns: calc((100% - (var(--s-4) * 3)) / 3.5);
  gap: var(--s-4);
}
@media (max-width: 1024px) {
  .carousel--dolls .carousel__track { grid-auto-columns: calc((100% - var(--s-4)) / 2.4); }
}
@media (max-width: 768px) {
  .carousel--dolls .carousel__track { grid-auto-columns: calc(100% / 1.35); }
}
.carousel--dolls { padding-right: 0; }
.carousel--dolls .carousel__track { padding-right: var(--s-4); }

/* Let the rail bleed off the right edge so it reads as continuing. */
.carousel--property { padding-right: 0; }
.carousel--property .carousel__track { padding-right: var(--s-4); }

/* The property section runs near-full-width — the tiles are the content, so
   the editorial margin that suits body copy just wastes the viewport here. */
/* ONE LEFT EDGE DOWN THE PAGE (Blake, 29 Sep 2026).

   Three bands stack under the hero and, until now, two columns ran through
   them. The collection tile names and the Instagram band sit at 20px on a
   phone and 40px from 1025px up - the gutter below 768, --s-5 above it. The
   property band sat at a flat 24px at every width, because .wrap--flush
   pads with --s-4. Internally it was correct - the heading was aligned to
   its own tiles - but read vertically it was the one band indented
   differently from its neighbours, by 4px on a phone and 16px on a desktop.

   BOTH CHILDREN MOVE TOGETHER, and that is the whole point: the section
   head and the carousel are separate .wrap--flush elements, and padding
   only the heading would put the page right and break the heading's
   register with the first tile it sits over. The child combinator takes
   both and nothing else.

   Scoped to #properties rather than changed on .wrap--flush itself. The
   only other user is shop-all, which is parked and unreviewed; moving it
   six days before launch would be risk without a reason. If shop-all is
   ever brought in, it should adopt this column too.

   The values are not literals - they are the same tokens .ctile__body uses,
   so if either is retuned the two keep tracking each other. */
#properties > .wrap--flush { padding-inline: var(--s-5); }
@media (max-width: 768px) {
  #properties > .wrap--flush { padding-inline: var(--gutter); }
}

#properties .section-head { margin-bottom: var(--s-5); }

/* CHOOSE YOUR OBSESSION — its own band.
   The section sits one step darker than the page, and the inactive tile field
   moves by exactly the same amount (−9 on each channel) so the tiles keep the
   same separation from their background that they had on paper.
     page  #F2F2F2 → band #E9E9E9
     tile  #DCDCDC → tile #D3D3D3
   Both stay strictly neutral (R = G = B), which tools/qa.js enforces. */
#properties { background: #E9E9E9; }
#properties .prop-tile .ph { background-color: #D3D3D3; }

/* NO STROKE HERE.
   The display heading thins itself by stroking the glyphs in the PAGE colour
   — which only works while the surface behind it IS the page colour. On this
   darker band the paper-coloured stroke would print a halo around every
   letter, so the heading runs at its natural weight instead. */
#properties .section-head__title--display { -webkit-text-stroke: 0; }

/* Slot labels sit top-left in this section, not centred — the tile name
   appears over the centre on hover and the two were stacking up. */
#properties .prop-tile .ph {
  align-items: flex-start;
  justify-content: flex-start;
  text-align: left;
  padding: var(--s-3);
}


/* ==========================================================================
   BROWSE THE COLLECTIONS — full-bleed split
   Two panels edge to edge, zero margin, zero gap, type overlaid on the image.
   No headline: section 2 above is already "Shop by Collection".

   (Two alternate layouts — editorial offset, stacked bands — were built for
   comparison and removed once this one was chosen.)
   ========================================================================== */

.collections { padding-block: 0; }

.collections__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 0;
}

.ctile {
  position: relative;
  display: block;
  overflow: hidden;
}

.ctile__media { overflow: hidden; }

/* Deliberately NOT full-viewport. An explicit clamped height keeps the band
   substantial without swallowing a whole screen, and stops the panels going
   enormous on wide monitors where a 3:4 ratio would compute ~960px tall. */
/* FIXED RATIO, NOT VIEWPORT HEIGHT (21 Sep 2026).
   These were sized in svh, so the shape changed with the browser WINDOW —
   1.08:1 on a laptop, 1.35:1 at 1920, portrait on a phone, and it moved
   again if you opened a bookmarks bar. There was no ratio to give JAKKS.
   Now a real ratio at each breakpoint, with the old clamp bounds kept as
   min/max-height so the band still cannot swallow a wide monitor. Anything
   outside the ratio is absorbed by object-fit: cover below. */
.ctile__media .ph {
  aspect-ratio: 4 / 3;
  height: auto;
  min-height: 489px;
  max-height: 776px;
}
.ctile__media .ph--filled img { transition: transform 900ms var(--ease); }
.ctile:hover .ctile__media .ph--filled img { transform: scale(1.05); }

/* Slot label top-left, matching the property band. Centred, it sat directly
   behind the collection name in the overlay below. */
.ctile__media .ph {
  align-items: flex-start;
  justify-content: flex-start;
  text-align: left;
  padding: var(--s-5) var(--gutter);
}

.ctile__body {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--s-6) var(--s-5);
  color: var(--dh-invert);
  background: linear-gradient(to top, rgba(18,18,18,0.78) 0%, rgba(18,18,18,0) 100%);
}

.ctile__name {
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: var(--t-h2);
  line-height: var(--t-h2-lh);
  letter-spacing: var(--t-h2-ls);
  color: var(--dh-invert);
}

.ctile .link-arrow {
  margin-top: var(--s-3);
  color: var(--dh-invert);
}

/* A hairline seam between the two panels, nothing more. */
.ctile + .ctile { box-shadow: -1px 0 0 rgba(255,255,255,0.16); }

/* The seam rule above is (0,2,0) and the global :focus-visible ring is
   (0,1,0), so the seam was winning and the focused tile lost the ink half of
   its dual ring - it measured 1.12:1, the only element on the site still
   under the 3:1 minimum after the focus rework. Restated here at matching
   specificity, after it, so focus wins while the seam survives unfocused.
   Any other component that sets its own box-shadow needs the same line. */
.ctile:focus-visible { box-shadow: 0 0 0 2px var(--dh-ink); }

@media (max-width: 768px) {
  .collections__inner { grid-template-columns: 1fr; }
  .ctile__media .ph {
    aspect-ratio: 4 / 5;
    height: auto;
    min-height: 431px;
    max-height: 604px;
  }
  .ctile__body { padding: var(--s-5) var(--gutter); }
  .ctile + .ctile { box-shadow: 0 -1px 0 rgba(255,255,255,0.16); }
}


/* --------------------------------------------------------------------------
   4. EDITORIAL BAND — full-bleed, Chocogom pull-quote overlay
   -------------------------------------------------------------------------- */

.editorial {
  position: relative;
  overflow: hidden;
  background: var(--dh-ink);
}
.editorial__slot .ph {
  aspect-ratio: 2 / 1;
  /* +15% on the previous 504 / 69svh / 780 figures. */
  height: auto;
  min-height: 580px;
  max-height: 897px;
  /* A `min-height: 0` sat here until 24 Sep 2026. It was correct when this
     rule set `aspect-ratio: auto` and an explicit `height: clamp(...)` — it
     cancelled the floor inherited from the base .ph. The 21 Sep change to a
     fixed 2:1 moved the floor INTO this rule as min-height: 580px and left
     the old line below it, so the floor it had just set was overridden to
     zero. Between 1025 and 1160px the band rendered 512-580px instead of
     being held at 580. Removed. */
  background-color: #2E2E2E;
  /* Callout sits TOP-LEFT, clear of the pull-quote below it. */
  align-items: flex-start;
  justify-content: flex-start;
  text-align: left;
  padding: var(--s-5) var(--gutter);
}
.editorial__slot .ph__label,
.editorial__slot .ph__meta { color: var(--dh-invert-70); }

.editorial__overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--s-5);
  text-align: left;
  padding: var(--s-6) var(--gutter);
  color: var(--dh-invert);
}

.editorial__quote {
  max-width: 22ch;
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: clamp(28px, 4.4vw, 60px);
  line-height: 1.16;
  letter-spacing: 0.045em;
  color: var(--dh-invert);
}

/* ™ ON THE PULL-QUOTE — smaller, and tight to the S (client via Blake,
   29 Sep 2026: "they still want the TM there, but are asking if it can be
   made smaller and closer to the S").

   The global .tm is 0.42em with a 0.08em left margin, tuned for Chocogom at
   headline scale. This line runs to 60px, so that is a 25px mark sitting
   4.8px clear of the wordmark - and the quote's own 0.045em letter-spacing
   adds another 2.7px of trailing air after the S before the margin even
   starts. Measured, that is 7.5px of gap, which is what "the spacing looks
   off" was describing.

   Both are addressed:
     font-size   0.42em -> 0.28em   (25px -> 17px at this size)
     margin-left 0.08em -> -0.16em  (in the MARK's em: -0.16 x 17px = -2.7px,
                                     exactly cancelling the inherited
                                     letter-spacing, so the mark sits against
                                     the S rather than after a gap)
     transform   -1.15em -> -1.72em (the shift is expressed in the mark's own
                                     em, so shrinking the mark has to scale it
                                     up by the same ratio, 0.42/0.28, to hold
                                     the same cap line)

   If the client would rather have it back exactly as the original reference,
   delete this whole rule - the global .tm is that treatment. */
.editorial__quote .tm {
  font-size: 0.28em;
  margin-left: -0.16em;
  transform: translateY(-1.72em);
}

.editorial__quote { margin-bottom: var(--s-6); }
@media (max-width: 768px) { .editorial__quote { margin-bottom: var(--s-4); } }
.editorial__cta { width: min(420px, 100%); }

/* OUTLINE AT REST, SOLID ON HOVER (Blake, 29 Sep 2026). The base .btn is a
   filled ink slab, which over this photograph read as a black rectangle
   dropped on the picture. Hollow, it belongs to the image; filling on hover
   is the reward for reaching it. Same treatment as the hero CTA, which is
   the other button that sits over photography. */
.editorial__cta .btn {
  background: transparent;
  color: var(--dh-invert);
  border: 1px solid var(--dh-invert);
}
.editorial__cta .btn:hover,
.editorial__cta .btn:focus-visible {
  background: var(--dh-invert);
  color: var(--dh-ink);
  border-color: var(--dh-invert);
}
.editorial__cta .cta-retail {
  background: var(--dh-invert);
  color: var(--dh-ink);
  border-color: var(--dh-invert);
}
.editorial__cta .cta-retail:hover { background: transparent; color: var(--dh-invert); }

@media (max-width: 768px) {
  .editorial__slot .ph {
    aspect-ratio: 3 / 5;
    height: auto;
    min-height: 518px;
    max-height: 794px;
  }
}


/* --------------------------------------------------------------------------
   SECTION HEADS — shared by the homepage and the template pages
   -------------------------------------------------------------------------- */

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-5);
  flex-wrap: wrap;
  margin-bottom: var(--s-6);
}
/* Section titles never wrap — "More from DC Super-Villains" on two lines
   reads as a mistake. They shrink instead of breaking. */
.section-head__title {
  max-width: none;
  white-space: nowrap;
}
/* PHONES MAY WRAP.
   The no-wrap rule exists so "More from DC Super-Villains" does not break
   across two lines on a desktop grid. At 440px holding that promise means
   either clipping the heading — which is what was happening, "EXPLORE WARNER
   BROS DOLL C…" — or shrinking Chocogom below body-copy size. Neither is
   better than a two-line heading on a phone, and the strings come from the
   data layer, so any new property name would break a hand-tuned size again. */
@media (max-width: 768px) {
  .section-head__title,
  .section-head__title--display {
    white-space: normal;
    text-wrap: balance;
  }
  .section-head__title { font-size: clamp(20px, 6vw, var(--t-h2)); }

  /* Tighter on phones. --section-y is editorial breathing room built for a
     desktop column; stacked on a 440px screen the same figures put most of a
     viewport of nothing between a rail and the heading of the next one.

     The worst of it was where the white product zone meets the grey band
     below: .pdp__layout's bottom padding and the next section's top padding
     stack on top of each other, so the seam carried two full desktop
     section gaps back to back. */
  .section-head { margin-bottom: var(--s-3); }
  .pdp__layout { padding-bottom: var(--s-5); }
  .pdp .section { padding-block: var(--s-5) var(--s-4); }
  .explore-block { padding-block: var(--s-5); }
}

/* +8px on the standard h2. */
.section-head__title--lg { font-size: calc(var(--t-h2) + 8px); }

/* DISPLAY-SCALE SECTION HEADING — the largest type on the page, and
   deliberately LIGHTER than the hero headline.

   Chocogom ships one weight, so there is no lighter cut to switch to. The
   trick is the inverse of how SHOP is bolded: stroking the glyphs in the
   PAGE COLOUR (with the stroke painting over the fill, i.e. no paint-order
   override) erodes the outline inward and genuinely thins the letterforms.
   Same face, visibly lighter than the hero. */
.section-head__title--display {
  max-width: none;
  /* WRAPS, at every width — and it must say so EXPLICITLY, because this
     element also carries the base .section-head__title class, which sets
     nowrap. Simply omitting the declaration here was not enough: the base
     still applied, and "Warner Bros. Doll Collection" ran 1430px wide at
     display size, overflowing a 1440px desktop.

     These strings come from client-supplied data, so their length is not
     ours to promise — the collection was renamed twice already. Balanced
     wrapping is the only rule that survives new copy arriving. */
  white-space: normal;
  text-wrap: balance;
  font-size: var(--t-display);
  line-height: 1.02;
  letter-spacing: 0.02em;
  -webkit-text-stroke: 0.5px var(--dh-paper);
}
@media (max-width: 1024px) {
  .section-head__title--display { -webkit-text-stroke-width: 0.35px; }
}
@media (max-width: 768px) {
  .section-head__title--display {
    font-size: clamp(28px, 8.5vw, var(--t-display));
    -webkit-text-stroke-width: 0.25px;
    /* MUST be repeated here. The mobile wrap rule further up is overridden
       by .section-head__title--display's own base block, which is declared
       LATER in this file at the same specificity — so "Choose Your
       Obsession" kept its desktop nowrap and ran 443px wide in a 440px
       viewport. Same class of bug as the other nowrap headings: nowrap is a
       desktop promise, and a phone cannot keep it. */
    white-space: normal;
    text-wrap: balance;
  }
}
.section-head__lede {
  max-width: 46ch;
  font-family: var(--f-body);
  font-weight: 300;
  font-size: var(--t-body-lg);
  line-height: 1.6;
  color: var(--dh-ink-70);
}


/* ==========================================================================
   COLLECTION PAGE
   Masthead, full-bleed banner, one grid per property, closing retail link.

   NOT PRESENT, ANYWHERE: filters, sort, price range, "load more", facets,
   result counts used as a sales device. This is a catalog.
   ========================================================================== */

/* --------------------------------------------------------------------------
   COLLECTION HERO
   One full-bleed band. The image is the whole thing; the mark, the headline
   and its subtitle float on top of it — nothing sits in a column beside it.
   -------------------------------------------------------------------------- */

.col-hero {
  position: relative;
  isolation: isolate;
  background: var(--dh-ink);
}

/* HALF HEIGHT WHILE THE SLOT IS EMPTY (Blake, 29 Sep 2026: "reduce the
   height by 50% of the title area, when there is no bg image" - reversing
   the 26 Sep position that the band should keep its height even empty).

   The reduction is CONDITIONAL, and it restores itself: these rules are the
   empty state, and the .ph--filled pair below puts every value back the
   moment JAKKS' collection hero artwork lands in the slot. Nothing has to be
   remembered or undone later.

   The furniture scales with the band, not just the box. A 97px licence mark
   and 40px insets were drawn for a 405px band; left alone in a 202px one
   they would have collided with the headline - measured, at 1024 and 1440,
   before this was written. So the mark, the top inset and the bottom inset
   all come down in the empty state too, and go back up with the artwork.

   :has() is what reads "the slot is filled" from outside the slot. */
.col-hero__slot .ph {
  /* 64:9 — half the height of the 32:9 band this shows once filled. */
  aspect-ratio: 64 / 9;
  height: auto;
  /* 190, not 160. Half of the filled band's 320 floor would be 160, and at
     1024 - the one width where the floor governs rather than the ratio - a
     160px band put the licence mark 4px ON TOP of the headline. Measured on
     the deployed page, not guessed. Above 1024 the 64:9 ratio governs and
     the band is exactly half: 203px at 1440, 270px at 1920. */
  min-height: 190px;
  background-color: #3A3A3A;
  /* The slot's own callout would collide with the floating type. */
  visibility: hidden;
}
.col-hero__slot .ph--filled {
  aspect-ratio: 32 / 9;
  min-height: 320px;
}

/* A scrim, not a wash. Type floats at the top and bottom corners, so the
   darkening is strongest exactly there and lifts away through the middle
   where the product photography needs to be seen. */
.col-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(18,18,18,0.55) 0%, rgba(18,18,18,0) 38%),
    linear-gradient(to top,    rgba(18,18,18,0.72) 0%, rgba(18,18,18,0) 46%);
}

/* MARK — floats top-left. */
/* ANCHOR TARGET (25 Sep 2026). The tray's "View {property}" buttons land on
   these sections, and the header is fixed once stuck — without a scroll
   margin the heading arrives underneath it. --header-h-stuck is the same
   token the PDP columns offset against, plus a little air so the section
   reads as a new start rather than as something jammed against the bar. */
.col-group { scroll-margin-top: calc(var(--header-h-stuck) + var(--s-4)); }

/* COLLECTION GRID (client, 26 Sep 2026) — replaced the horizontal rail,
   because a rail hides everything past the third tile behind an interaction
   the visitor has to spot first.

   Three across, two on a phone, so a five-doll property reads 3 over 2. The
   last row being short is the accepted outcome, not a bug: the alternative
   is padding the row with something that is not a product.

   minmax(0, 1fr) on the tracks, per the house rule — a nowrap placeholder
   label will otherwise blow a track out past the grid. */
.col-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s-5) var(--s-4);
}
@media (max-width: 768px) {
  .col-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--s-4) var(--s-3);
  }
}

.col-hero__mark {
  position: absolute;
  z-index: 2;
  top: var(--s-4);
  /* RIGHT, NOT LEFT (client via Blake, 29 Sep 2026: "on collection pages,
     move the collection logo to the right side"). It sat top-left, directly
     above the headline, which stacked two marks of attention in one corner.
     Opposite the headline it balances the band instead. object-position
     follows it, or a mark narrower than its box would drift back left. */
  right: var(--gutter);
  display: block;
  /* Ratio follows the trimmed file (26 Sep 2026) — 168 x 145, the shield's
     own bounding box. It was 2:1 to match the untrimmed canvas. */
  /* Compact while the slot is empty; the rule below restores the full
     clamp once the artwork arrives. */
  /* Same reasoning as .pdp__logo: height matches, width follows the mark. */
  /* 2x (Blake, 29 Sep 2026: "make the logo in the title bar on collection
     pages, 2x in height"). The clamp's ceiling and its vw rate both double,
     34/4.3/59 -> 40/8.6/118, so every width from 1024 up gets a true 2x:
     59->118 at 1440, 55->110 at 1280, 44->88 at 1024.

     The FLOOR only goes 34 -> 40, not 68. Measured on both collection pages
     first: the mark is top-right and the headline bottom-left, so a vertical
     overlap is harmless while they are side by side - at 1440 the gap is 5px
     and it does not matter, because the mark's left edge is 223px clear of
     where the headline ends. Below about 1024 that stops being true. The
     Doll Haus of Dread headline is the long one and reaches furthest right,
     and at 900 its right edge passes UNDER the mark. A doubled floor would
     put a 68px mark on top of it.

     Checked by SCREENSHOT at 900 and 360 on both collection pages, not by
     the geometry alone: a bounding-box test flags a collision at both widths
     that does not exist, because a wrapped headline's box is as wide as its
     widest line while the line nearest the mark is much shorter. Same class
     of false positive as the crop artifacts earlier today. Nothing overlaps
     at any width from 1920 down to 360. */
  height: clamp(40px, 8.6vw, 118px);
  width: auto;
  max-width: 46vw;
  object-fit: contain;
  object-position: right center;
  /* NO brightness(0) (25 Sep 2026). That step forced every pixel to black
     and invert(1) then turned the lot white — a featureless shield with the
     WB knocked out of it. It was harmless while this slot held a named
     placeholder box, where a silhouette is all there is; the moment a real
     licensed mark landed here it flattened it.

     grayscale + invert alone still lifts a dark-on-transparent mark onto the
     dark hero, and keeps the internal contrast that makes it the WB shield
     rather than a blob. A licensed logo rendered as a silhouette is a brand
     problem as much as a visual one.

     NO MORE invert(1) (29 Sep 2026). That step existed to lift a DARK mark
     onto this dark band, which was the right guess while the only asset here
     was a solid colour shield. It is wrong for the asset this slot actually
     wants: rendered every way round, the white outline shield with NO invert
     is the only combination that reads here. invert turned it black on black.

     THE ASSET CONTRACT FOR THIS SLOT IS NOW EXPLICIT: it takes the WHITE /
     negative version of a mark. That is already what the JAKKS ask says for
     the two collection hero marks still outstanding, so the code and the
     brief now agree instead of the CSS quietly compensating for the wrong
     file. grayscale stays, because the palette guarantee is site-wide and a
     white mark is unchanged by it. */
  filter: grayscale(1);
}

/* Named client-asset slot, where no mark has been supplied. */
.col-hero__mark--slot {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-2);
  background: rgba(255,255,255,0.12);
  border: 1px dashed rgba(255,255,255,0.4);
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 9px;
  line-height: 1.4;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--dh-invert-70);
  text-align: center;
  filter: none;
}

/* HEADLINE + SUBTITLE — float bottom-left, as one block. */
.col-hero__copy {
  position: absolute;
  z-index: 2;
  left: var(--gutter);
  bottom: var(--s-4);
  right: var(--gutter);
  color: var(--dh-invert);
}

/* FULL SIZE COMES BACK WITH THE ARTWORK. One place, so the empty state and
   the filled state can never drift apart. */
/* The filled band is twice the height of the empty one, so its mark doubles
   on the same terms: 48/6/84 -> 56/12/168. */
.col-hero:has(.ph--filled) .col-hero__mark {
  height: clamp(56px, 12vw, 168px);
  top: var(--s-5);
}
.col-hero:has(.ph--filled) .col-hero__copy { bottom: var(--s-5); }

.col-hero__title {
  margin: 0;
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: clamp(34px, 6.4vw, var(--t-h1));
  line-height: 1.02;
  letter-spacing: var(--t-h1-ls);
  text-wrap: balance;
}

/* COLLECTION reads as the headline's subtitle, so it sits under it and
   picks up the label treatment rather than being a second heading. */
.col-hero__sub {
  display: block;
  margin-top: var(--s-3);
  font-family: var(--f-body);
  font-weight: var(--t-label-w);
  font-size: var(--t-label);
  line-height: 1;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--dh-invert-70);
}

@media (max-width: 768px) {
  .col-hero__slot .ph { aspect-ratio: 2 / 1; height: auto; min-height: 130px; }

  /* THE SQUARE BOX IS ONLY SAFE WITH AN ART-DIRECTED MOBILE CROP
     (29 Sep 2026).

     This band is object-fit: cover, and a phone put it at 1 / 1. JAKKS were
     told the landscape crop is 32:9. Measured: a 32:9 file in a 1 / 1 cover
     box keeps 28.1% of its width. A 72% crop, invisible today only because
     the slot is empty, and due to appear the day the artwork landed.

     placeholder() emits a <picture> only when a mobile source exists, so the
     markup already says which case this is. :has() reads it:

       with a mobile crop     the square box is what that file was shot for
       one landscape file     keep the file's own shape and lose nothing

     The second case contains rather than covers, against --dh-ink. The band
     is ink, so the space above and below the strip is the band, not a
     letterbox - it reads as a cinematic band rather than a broken fit. The
     260px floor stays either way: it is what keeps the mark off the
     headline, which was measured at 1024 back on 26 Sep. */
  .col-hero__slot .ph--filled:has(picture) { aspect-ratio: 1 / 1; min-height: 260px; }

  .col-hero__slot .ph--filled:not(:has(picture)) {
    aspect-ratio: 1 / 1;
    min-height: 260px;
    background-color: var(--dh-ink);
  }
  .col-hero__slot .ph--filled:not(:has(picture)) img {
    object-fit: contain;
    object-position: center;
  }
  .col-hero__mark { top: var(--s-3); }
  .col-hero__copy { bottom: var(--s-3); }
  .col-hero:has(.ph--filled) .col-hero__mark { top: var(--s-4); }
  .col-hero:has(.ph--filled) .col-hero__copy { bottom: var(--s-4); }
  .col-hero__sub { margin-top: var(--s-2); letter-spacing: 0.18em; }
}

/* CLIENT-TO-SUPPLY FLAG
   The WB headline is marked as a placeholder in the docx. It renders as a
   visible, deliberately plain flag rather than being quietly swapped for the
   collection name — a silent fallback is how placeholder copy ships. */
.col-head__flag {
  display: inline-block;
  margin-top: var(--s-5);
  padding: var(--s-2) var(--s-3);
  border: 1px dashed var(--dh-ink-45);
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dh-ink-45);
}

.col-head__flag + * { margin-top: var(--s-4); }

.col-boiler { padding-block: var(--s-6); }

.col-groups { padding-block: var(--s-6); }
.col-group + .col-group { margin-top: var(--s-7); }

/* The "View {property}" link rides on the heading, so the head stays tight
   to the rail it introduces. */
.col-group .section-head { margin-bottom: var(--s-4); }

.col-close {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  flex-wrap: wrap;
  padding-block: var(--s-6) var(--s-8);
  border-top: var(--hair);
}
.col-close__line { max-width: 22ch; }
.col-close__cta  { width: min(420px, 100%); }

@media (max-width: 768px) {

  .col-boiler, .col-groups { padding-block: var(--s-5); }
  .col-group + .col-group { margin-top: var(--s-6); }
  .col-head__row { gap: var(--s-4); }
  .col-close { flex-direction: column; align-items: stretch; }
}


/* ==========================================================================
   SHOP ALL
   The whole range as horizontal rails — one rail per property, grouped
   under its collection. Rails, not a wall of tiles: eighteen dolls in a
   flat grid reads as a warehouse.
   ========================================================================== */

.shopall__band + .shopall__band { margin-top: var(--s-8); }

.shopall__band-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-3);
  padding-block: var(--s-7) var(--s-5);
  border-top: var(--hair);
}
.shopall__band-meta { margin-top: var(--s-1); }

.shopall__row { padding-block: var(--s-5); }
.shopall__row .section-head { margin-bottom: var(--s-4); }
/* Property heads are subordinate to the collection band above them. */
.shopall__row .section-head__title { font-size: calc(var(--t-h2) - 8px); }

@media (max-width: 768px) {
  .shopall__band + .shopall__band { margin-top: var(--s-7); }
  .shopall__band-head { padding-block: var(--s-6) var(--s-4); }
  .shopall__row .section-head__title { font-size: clamp(18px, 5.5vw, var(--t-h3)); }
}


/* ==========================================================================
   PRODUCT PAGE (PDP)
   White sweep throughout the product zone — this is what makes the product
   read as gallery-lit.

   NOT PRESENT, ANYWHERE: quantity selector, variant picker, add-to-cart,
   shipping accordion, reviews, star ratings, stock status, delivery promise.
   ========================================================================== */

.pdp { background: var(--dh-sweep); }

.pdp__layout {
  display: grid;
  grid-template-columns: 88px minmax(0, 1fr) minmax(360px, 34%);
  gap: var(--s-5);
  align-items: start;
  padding-bottom: var(--section-y);
}

@media (max-width: 1280px) {
  .pdp__layout { grid-template-columns: 72px minmax(0, 1fr) minmax(320px, 36%); gap: var(--s-4); }
}
/* MOBILE / TABLET — thumbnail rail LEFT, featured image right, info below.

   minmax(0, …) on the image track is load-bearing. A bare `1fr` floors at the
   track's min-content, and .pdp__swipe is a grid whose `grid-auto-columns:
   100%` cannot resolve during intrinsic sizing — so it reported ~867px of
   min-content and the single track grew to 827px inside a 400px page. That
   is the "everything is cut off on mobile" bug: the whole product layout was
   twice the width of the phone. */
@media (max-width: 1024px) {
  .pdp__layout {
    grid-template-columns: 64px minmax(0, 1fr);
    gap: var(--s-3);
    /* The air between the image and the brand lockup below it is THIS gap,
       not .pdp__brand's margin — the lockup is the first thing in the second
       grid row. Enough to separate the two blocks, not enough to read as a
       break in the page. */
    row-gap: var(--s-4);
  }
  .pdp__rail        { grid-column: 1; grid-row: 1; }
  .pdp__col--gallery{ grid-column: 2; grid-row: 1; min-width: 0; }
  .pdp__col--info   { grid-column: 1 / -1; grid-row: 2; min-width: 0; }
}
@media (max-width: 480px) {
  .pdp__layout { grid-template-columns: 52px minmax(0, 1fr); }
}


/* --------------------------------------------------------------------------
   THUMBNAIL RAIL — sticky, far left, 88px
   -------------------------------------------------------------------------- */

.pdp__rail {
  position: sticky;
  top: calc(var(--header-h) + var(--s-4));
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
}

.pdp-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: 0;
  background: #F4F4F4;
  outline: 2px solid transparent;
  outline-offset: -2px;
  transition: outline-color var(--dur) var(--ease), opacity var(--dur) var(--ease);
  opacity: 0.55;
  overflow: hidden;
}
.pdp-thumb:hover { opacity: 0.85; }
.pdp-thumb.is-active { opacity: 1; outline-color: var(--dh-ink); }

.pdp-thumb--img { opacity: 0.55; background: var(--dh-sweep); }
.pdp-thumb--img.is-active { opacity: 1; }
.pdp-thumb img { width: 100%; height: 100%; object-fit: contain; }

.pdp-thumb__n {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 11px;
  letter-spacing: 0.1em;
  color: var(--dh-ink-45);
}

/* MOBILE RAIL
   Same stack as desktop, but bounded to the height of the featured square
   beside it and allowed to scroll. The fade is the affordance: it only shows
   when there is more below, so a product with two shots looks finished and a
   product with five looks like it continues. */
@media (max-width: 1024px) {
  .pdp__rail {
    position: static;
    align-self: stretch;
    height: 100%;
    gap: var(--s-2);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    /* Feathered bottom edge. mask-image is ignored where unsupported, which
       degrades to a hard edge — the rail still scrolls either way. */
    -webkit-mask-image: linear-gradient(to bottom, #000 82%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 82%, transparent 100%);
  }
  .pdp__rail::-webkit-scrollbar { width: 0; height: 0; }
  .pdp-thumb { flex: 0 0 auto; }
}


/* --------------------------------------------------------------------------
   GALLERY — deliberately oversized. Editorial plates, not thumbnails.
   -------------------------------------------------------------------------- */

.pdp__gallery { display: grid; gap: var(--s-4); }

.pdp-plate {
  display: block;
  width: 100%;
  padding: 0;
  background: none;
  cursor: zoom-in;
  overflow: hidden;
}
.pdp-plate .ph { background-color: #F4F4F4; }
.pdp-plate .ph--filled img { transition: transform var(--dur-tile) var(--ease); }
.pdp-plate:hover .ph--filled img { transform: scale(1.02); }

/* Mobile: full-bleed swipeable carousel with a dot rail and a 1 / 5 counter */
.pdp__mobile-gallery { display: none; }

@media (max-width: 1024px) {
  .pdp__gallery { display: none; }

  /* No longer full-bleed: the thumbnail rail sits to its left, so the
     featured image is bounded by its own track. min-width:0 stops the swipe
     grid from pushing that track open. */
  .pdp__mobile-gallery { display: block; min-width: 0; }

  .pdp__swipe {
    display: grid;
    min-width: 0;
    grid-auto-flow: column;
    grid-auto-columns: 100%;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .pdp__swipe::-webkit-scrollbar { display: none; }
  .pdp__swipe > * { scroll-snap-align: center; }
  .pdp__swipe .ph { background-color: #F4F4F4; }

  .pdp__swipe-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-4);
    padding: var(--s-3) 0 0;
  }
  /* THE WHOLE META ROW IS GONE — dots and the "1 / 5" counter both.
     The thumbnail rail beside the image already shows how many shots there
     are AND which one you are on, so the counter was a third statement of
     the same fact under a photograph that should be the only thing there.

     The markup still renders both so gallery.js keeps one code path and the
     lightbox counter keeps working; they are simply never shown. */
  .pdp__swipe-meta { display: none; }
  .pdp__dots { display: none; }
  .pdp__dots i {
    width: 6px; height: 6px;
    background: var(--dh-hairline);
    transition: background var(--dur) var(--ease);
  }
  .pdp__dots i.is-on { background: var(--dh-ink); }
  .pdp__counter {
    font-family: var(--f-body);
    font-size: 12px;
    letter-spacing: 0.08em;
    color: var(--dh-ink-45);
    font-variant-numeric: tabular-nums;
  }
}


/* --------------------------------------------------------------------------
   INFO COLUMN

   NOT STICKY. It used to be — `position: sticky` with a header-height offset,
   from before the columns became their own scroll containers. Once
   .pdp__col--info gained `overflow-y: auto`, that sticky child began sticking
   to the SCROLLER instead of the viewport: it pinned itself 150px down inside
   its own container and stayed there. scrollTop changed on every wheel event
   and nothing moved, which is exactly the "I can't scroll the right column"
   symptom — the content was cut off at the fold with no way to reach it.

   A scroll container's own child must not be sticky to that container.
   -------------------------------------------------------------------------- */

.pdp__info { position: static; }

/* --------------------------------------------------------------------------
   INDEPENDENT COLUMN SCROLLING — desktop only
   Each column is its own scroll container pinned to the viewport. The
   handoff order (right → left → page) is orchestrated in gallery.js, because
   native scroll chaining only ever goes child → page and cannot hop sideways
   to a sibling. overscroll-behavior:contain stops the browser chaining on its
   own so the script stays authoritative.
   -------------------------------------------------------------------------- */

@media (min-width: 1025px) {
  .pdp__col {
    position: sticky;
    top: calc(var(--header-h-stuck) + var(--s-3));
    height: calc(100svh - var(--header-h-stuck) - var(--s-5));
    overflow-y: auto;
    overscroll-behavior: auto;   /* see note above — never `contain` */
    scrollbar-width: thin;
  }
  /* The scrollbars are furniture — hide them, the content is the interface. */
  .pdp__col::-webkit-scrollbar { width: 0; height: 0; }
  .pdp__col { scrollbar-width: none; -ms-overflow-style: none; }

  /* Room so the last plate / accordion is not flush against the fold.
     The info column gets a deep tail on purpose: the handoff to the gallery
     only feels right if the reader can see they have finished the column
     before it starts driving the other one. */
  .pdp__col--gallery { padding-bottom: var(--s-6); }
  .pdp__col--info    { padding-bottom: var(--s-9); padding-right: var(--s-2); }
}

/* BRAND LOCKUP — mark and property name on one baseline.

   The mark sits in a SQUARE 1:1 slot. That also solves the alignment problem
   the old negative-margin trim was patching: the supplied WB file is a
   300×150 canvas with the shield centred, so it carries a wide band of
   transparent margin each side. `object-fit: cover` in a square box crops
   that dead space away automatically — no magic numbers, and it self-corrects
   if JAKKS supplies a differently-proportioned asset. */

.pdp__brand {
  /* STACKED, NOT SIDE BY SIDE (Blake, 29 Sep 2026: "add a line break after
     the logo"). Side by side, the mark and the property name competed for one
     449px row: Hellraiser's 8:1 wordmark took the full 220px cap and pushed
     "DOLL HAUS OF DREAD" onto two lines, and every mark was squeezed by
     whatever the name needed. Stacked, the mark gets the row to itself and the
     name gets a clean baseline under it. */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* 10px UNDER THE MARK (Blake, 30 Sep 2026: "add 10px padding under the logos
     on the product pages").

     The history, because this line has moved three times in two days: it was
     var(--s-2) (8px), went to a literal 20px on 29 Sep at Blake's number, came
     back off on 30 Sep ("remove the padding i added under the logos"), and is
     now 10px. Read his number as the SPACE, not as something added on top of
     the 8 - that is how 20px was read and he did not correct it.

     It is the stack's gap, so it IS the space under the mark; a margin on
     .pdp__logo would add to this rather than replace it, which is the trap.
     10 is off the --s scale (4 / 8 / 16 / 24) on purpose, at Blake's number,
     so it is written as a literal rather than rounded to var(--s-2). The two
     extreme-ratio marks hug their artwork (see the floor block below), so this
     is the real gap under every mark, not just the ones that fill their box. */
  gap: 10px;
  /* NO TOP MARGIN on desktop. Both columns start at the same y inside the
     grid, so any air here dropped the lockup below the first gallery plate.
     The top of the mark now lines up with the top of the featured image. */
  margin: 0 0 var(--s-5);
  color: var(--dh-ink);
}

/* CONTAIN, NOT COVER (Blake, 25 Sep 2026) — "the left and right side of
   this WB logo is cut off."

   The box was 1:1 with object-fit: cover, and the asset is
   WB_LOGO_300x150.png — 2:1. cover scales to FILL the shorter axis, so it
   matched the 80px height and threw away the overflowing width: the shield
   lost its left and right edges. The old comment said cover was "cropping
   the asset's transparent margin", which was true of a squarer file and
   stopped being true when this one replaced it.

   contain fits the whole mark inside the box. The box keeps its 80px square
   footprint so the row it sits in does not reflow — a 2:1 mark simply
   letterboxes to 80 x 40 inside it and centres. */
.pdp__logo {
  /* THE BOX MATCHES THE ASSET, 2:1 (25 Sep 2026).

     Measured inside WB_LOGO_300x150.png rather than guessed: the shield is
     168 x 145 opaque pixels sitting in a 300 x 150 canvas, with 67px of
     empty space on the left and 65px on the right. So the file is 2:1 and
     44% of its width is margin JAKKS baked in.

     A 1:1 box was wrong either way. With cover it filled the 80px height
     (160px wide) and then cropped to 80 - taking 40px off each side, which
     ate the shield's edges: Blake's "the left and right side is cut off".
     With contain it fitted the whole canvas into 80 x 40 and the shield came
     out about 45px, uncropped but half the intended size.

     THE FILE IS TRIMMED NOW (26 Sep 2026). Localising the asset was the
     moment to drop the 44% of transparent canvas JAKKS had baked around the
     shield: assets/img/warner-bros-logo.png is the artwork's own bounding
     box, 168 x 145. The mark is untouched — only the empty space around it
     is gone — so the box is the shield's ratio rather than the file's, and
     the gap that used to sit between the mark and the name goes with it.

     contain stays rather than a tuned cover: a cropped third-party mark is a
     licensing problem, not a visual one, and contain is safe whatever shape
     JAKKS supplies next. */
  /* SIZED BY HEIGHT, RATIO LEFT TO THE ARTWORK (29 Sep 2026). This box used
     to hard-code 168/145 — the Warner Bros. shield's own bounding box — back
     when the shield was the only logo that ever landed in it. The Doll Haus
     of Dread mark arrived at 637x322, nearly twice as wide as it is tall, and
     a fixed ratio either letterboxes it or squashes it.
     Height is the thing that must match across logos so they sit on one
     optical line; width is the artwork's business. */
  /* 230px (Blake, 29 Sep 2026: "limit the width of the logos on the product
     pages to 230 px").

     Earlier the same day this went 220px -> 300px -> 100% of the column, to
     stop Hellraiser's 8.1:1 wordmark containing to a third of the cap height
     the other marks got. 230px reverses that on purpose: it trades optical
     cap height on the two very wide marks for a consistent maximum footprint
     across all twelve pages.

     What it costs, measured rather than estimated:
       hellraiser   8.10:1   449x80 -> 230x28
       friends      6.05:1   449x80 -> 230x38
       scooby       3.00:1   240x80 -> 230x77
     Everything else is narrower than 230 at h80 and does not touch the cap:
     Ghost Face 206, Doll Haus of Dread 159, Warner Bros. 93, Saw 84.

     WHY 230 SCALES THE WAY IT DOES. Measured on the rendered page, the
     LONGEST property name - "Doll Haus of Dread(TM)" - has an ink width of
     236px from 1280 up and 209px at 768 and below, following --t-h3 as it
     steps down with the type scale. 230 is 97.5% of 236, so no mark is ever
     wider than the longest name in the set. That ratio is what the breakpoint
     below keeps: 209 x 0.975 = 204, so 205px once the type steps down.

     Not "no wider than its own name": a short name like FRIENDS(TM) is far
     narrower than 230, so those marks do overhang their own label, by design.
     The cap is a consistent maximum footprint across twelve pages, not a
     per-page relationship.

     The 1024 breakpoint needs no value of its own - the layout changes there
     but the type scale does not, so the name is still 236 and 230 still fits
     under it. */
  /* WIDTH-FIRST, SO THE BOX HUGS THE ARTWORK VERTICALLY.

     This used to be a fixed 80px height with an auto width and a max-width
     cap, and that box
     stays 80px tall even when the cap has squeezed the artwork to 25px -
     object-fit then letterboxes inside it and leaves 27px of dead air under
     the mark. With the gap set to 10px that read as 37px under Hellraiser and
     10px under Ghost Face, which is the "padding under the logos" coming back
     by the side door.

     width-then-max-height puts the empty space on the RIGHT instead, where it
     costs nothing: a mark taller than the cap allows keeps a 205px-wide box
     and contains inside it (Saw draws 100x80 flush left), while a mark the cap
     squeezes gets a box its own height (Hellraiser 205x25). Every mark's
     artwork now sits on the bottom edge of its box, so 10px means 10px under
     all twelve. min(100%, …) so the cap can never beat the column on a phone. */
  width: min(100%, 205px);
  height: auto;
  max-height: 80px;
  flex: 0 0 auto;
  object-fit: contain;
  object-position: left center;
  /* ----------------------------------------------------------------------
     PARKED — reinstating colour on the brand marks
     (Blake, 29 Sep 2026: "make sure you still have the color versions of
     logos in the handoff so if the client chooses to make them color instead
     its an easy change".)

     DELETE THE ONE LINE BELOW and every mark returns to its supplied colour.
     Verified by rendering all seven mark types with the filter removed:

       Warner Bros.          gold and navy shield
       Scooby-Doo            purple #702F90 / orange #FAAB54
       Friends               black letters, red / cyan / yellow dots
       Ghost Face            black wordmark with the RED blood drip
       Doll Haus of Dread    black badge, white knockout type (one-colour)
       Saw, Hellraiser       black wordmarks (one-colour by nature)

     Every source file still carries its colour. Nothing was flattened to
     grey on disk, and the two marks that WERE recoloured (Saw and Hellraiser,
     white -> ink, because they are white artwork and this page is paper) keep
     their originals beside them as *-white.svg. Ghost Face kept its red for
     exactly this reason — it would have been simpler to make the whole mark
     ink, and it would have cost the drip.

     The palette's own colour-restore block is in tokens.css and is separate;
     this line is only the marks. The collection hero's mark carries its own
     filter (grayscale + invert, ~line 885) because that band is dark — it
     needs the same treatment there.
     ---------------------------------------------------------------------- */
  filter: grayscale(1);
}

/* JASON UNIVERSE KEEPS ITS RED (Blake, 30 Sep 2026: "the red color is missing
   in the logo, please fix").

   .pdp__logo carries filter: grayscale(1) - the PARKED colour treatment, the
   one line that takes every brand mark on the site to grey and that is meant
   to be deleted if the client chooses colour. The Jason mark was inverted
   earlier today with its #C92431 deliberately preserved, and then that filter
   greyed it on the page anyway. The red was in the file the whole time; it was
   never reaching the screen.

   filter: none on this one mark, so the slash on the hockey mask reads as red
   while the rest of the set stays parked in grey.

   NOTE FOR WHOEVER LIFTS THE PARK: Ghost Face carries a red blood drip in
   exactly the same situation and is still grey, by nature of the same filter.
   Deleting the grayscale line on .pdp__logo restores every mark at once and
   makes this exception redundant - it is safe to leave in place, since
   filter: none is what the mark would get anyway. */
/* The 1 Oct 2026 Jason mark (licence-jason-universe-logo.png) is one colour,
   with no red, so this exception no longer matches anything on the site. It
   stays for the 30 Sep vector, should the client want the red slash back. */
.pdp__logo[src$="licence-jason-universe-logo.svg"] { filter: none; }

/* THE PER-MARK FLOOR IS GONE (30 Sep 2026). For a few hours the cap carried a
   floor - no mark below 40px of artwork height - which gave Hellraiser a 325px
   cap of its own and Friends 245px. Blake replaced it with a flat 205px for
   every mark. The reasoning and the measurements are in the block above and in
   docs/CHANGELOG-build.md; nothing here overrides the cap any more. */

@media (max-width: 1024px) {
  /* Zero — the grid's row-gap above already does this job. */
  .pdp__brand { margin-top: 0; }
}

/* No breakpoint of its own any more. The cap was 230 on desktop and stepped
   down to 205 at 768 to track the property name's ink width as --t-h3 shrank.
   205 is the cap at every width now, so the step-down has nothing to do. */

/* Properties with no supplied mark get their named client-asset slot at the
   same height, so the lockup holds either way. */
.pdp__logo--slot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 80px;
  padding: 0 var(--s-2);
  background: var(--dh-paper-deep);
  filter: none;
}
.pdp__logo--slot span {
  font-family: var(--f-body);
  font-weight: 400;
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--dh-ink-45);
  text-align: center;
}

.pdp__brand-name {
  /* The 6px optical nudge that used to sit here aligned the cap line with the
     mark's lower edge, which only meant anything while the two were side by
     side. Stacked, it just pushed the name away from the mark. */
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: var(--t-h3);
  line-height: var(--t-h3-lh);
  letter-spacing: var(--t-h3-ls);
  transition: opacity var(--dur) var(--ease);
}
.pdp__brand:hover .pdp__brand-name { opacity: 0.58; }
/* With the name gone (1 Oct 2026) the mark is the whole link, so it carries
   the hover the name used to. Opacity, not a filter: .pdp__logo's filter line
   is the parked colour switch and must stay a one-line delete. */
.pdp__logo { transition: opacity var(--dur) var(--ease); }
.pdp__brand:hover .pdp__logo { opacity: 0.58; }

/* Touch target — this is a link to the property page, not a caption. */
.pdp__eyebrow { display: inline-flex; align-items: center; min-height: 44px; }
.pdp__eyebrow {
  display: inline-block;
  font-family: var(--f-body);
  font-weight: var(--t-label-w);
  font-size: var(--t-label);
  letter-spacing: var(--t-label-ls);
  text-transform: uppercase;
  color: var(--dh-ink-45);
  transition: color var(--dur) var(--ease);
}
.pdp__eyebrow:hover { color: var(--dh-ink); }

.pdp__name { margin-top: 0; }

.pdp__price {
  margin-top: var(--s-4);
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 24px;
  line-height: 1;
}

/* SKU — small and understated. Not a headline element. */
.pdp__sku {
  margin-top: var(--s-2);
  font-family: var(--f-body);
  font-weight: 400;
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--dh-ink-45);
}

.pdp__le { margin-top: var(--s-3); }

.pdp__cta { margin-top: var(--s-5); }

.pdp__desc { margin-top: var(--s-5); }
.pdp__desc p {
  font-family: var(--f-body);
  font-weight: 300;
  font-size: var(--t-body-lg);
  line-height: var(--t-body-lg-lh);
  color: var(--dh-ink-70);
}
.pdp__desc p + p { margin-top: var(--s-4); }


/* --------------------------------------------------------------------------
   PRODUCT DETAILS ACCORDION
   Closed by default on mobile, open by default on desktop.
   -------------------------------------------------------------------------- */

/* Pulled up now that the separator rule is gone. */
.pdp__details { margin-top: var(--s-5); }

.accordion { border-top: var(--hair); }
.accordion:last-child { border-bottom: var(--hair); }

.accordion__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  width: 100%;
  padding: var(--s-4) 0;
  text-align: left;
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: var(--t-h3);
  line-height: var(--t-h3-lh);
  letter-spacing: var(--t-h3-ls);
}
.accordion__toggle .icon {
  transition: transform var(--dur) var(--ease);
  flex: 0 0 auto;
}
.accordion.is-open .accordion__toggle .icon { transform: rotate(180deg); }

.accordion__panel { padding-bottom: var(--s-3); }
.accordion__panel[hidden] { display: none; }


/* --------------------------------------------------------------------------
   STICKY MOBILE BUY BAR
   Appears once the info-column CTA has scrolled out of view.
   -------------------------------------------------------------------------- */

.buybar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: calc(var(--z-tab) - 1);
  display: none;
  align-items: center;
  gap: var(--s-3);
  padding: 10px var(--gutter);
  background: color-mix(in srgb, var(--dh-sweep) 94%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border-top: var(--hair);
  box-shadow: 0 -8px 30px rgba(18, 18, 18, 0.07);
  transform: translateY(100%);
  transition: transform var(--dur) var(--ease);
  pointer-events: none;          /* the bar is a positioner, not a surface */
}
.buybar > * { pointer-events: auto; }
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .buybar { background: rgba(255, 255, 255, 0.94); }
}
.buybar.is-up { transform: none; }

/* NOT pinned from load at any width. The bar slides up only after the reader
   has scrolled past the real BUY button — see the observer in gallery.js.
   Showing it immediately put a black bar across the product photograph
   advertising a button the reader had not reached yet. */

.buybar__meta { min-width: 0; flex: 1; }
.buybar__name {
  font-family: var(--f-display);
  text-transform: uppercase;
  font-size: 13px;
  letter-spacing: 0.06em;
  line-height: 1.2;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
.buybar__price {
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 13px;
  color: var(--dh-ink-45);
  margin-top: 2px;
}
.buybar .cta-retail {
  width: auto;
  flex: 0 0 auto;
  height: 46px;
  padding-inline: var(--s-4);
  /* The tab lives at 30% from the bottom on mobile, so the bar clears it. */
  margin-right: 42px;
}
.buybar .cta-retail__ext { display: none; }

/* Present at every breakpoint, revealed by the same rule at both: once the
   in-column CTA has scrolled up out of view. Desktop shows product name and
   price on the left with BUY on the right; mobile shows the button alone. */
.buybar { display: flex; }

@media (max-width: 1024px) {
  /* MOBILE: the button floats on its own. The white bar behind it was there
     to carry the product name and price, and that meta is hidden at this
     width — so all it did was rule a white band across the bottom of the
     photograph. The bar keeps its position and its slide-in; it just stops
     painting anything. */
  .buybar {
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border-top: 0;
    box-shadow: none;
    padding: 0 var(--gutter) var(--s-3);
  }
  .buybar__meta { display: none; }
  .buybar .cta-retail { width: 100%; margin-right: 52px; }
}


/* --------------------------------------------------------------------------
   LIGHTBOX
   -------------------------------------------------------------------------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: var(--z-lightbox);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-6) var(--s-5);
  background: rgba(18, 18, 18, 0.94);
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur) var(--ease), visibility 0s linear var(--dur);
}
.lightbox.is-open {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur) var(--ease), visibility 0s;
}

/* THE PANEL HUGS THE PHOTOGRAPH (client via Blake, 29 Sep 2026: "when a
   product image is clicked on, it crops the square image").

   What the old rule did: the stage was a fixed 760px wide and the .ph kept
   the gallery's aspect-ratio while ALSO carrying max-height. In a short
   window the max-height wins and the ratio is violated, so a 1:1 shot sat
   in a 760 x 424 box - the image itself was contained and whole, but it
   floated in the middle of a wide #1E1E1E letterbox with the panel cut
   straight across above and below it. That is the crop being reported.

   Now the panel takes its size FROM the image: no forced ratio, no fixed
   width, no panel colour behind a filled slot. The photograph is bounded by
   the viewport on both axes and is always whole, at any window size, with
   no letterbox to mistake for a crop.

   An UNFILLED slot still needs a ratio - there is no image to take a size
   from - so it keeps the old treatment. */
.lightbox__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: auto;
  max-width: min(760px, 100%);
  max-height: 100%;
}
.lightbox__stage .ph {
  aspect-ratio: auto;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: calc(100svh - var(--s-7) * 2);
  background-color: transparent;
  overflow: visible;
}
.lightbox__stage .ph img,
.lightbox__stage .ph picture {
  display: block;
  width: auto;
  height: auto;
  max-width: min(760px, 100%);
  max-height: calc(100svh - var(--s-7) * 2);
  object-fit: contain;
}
.lightbox__stage .ph:not(.ph--filled) {
  aspect-ratio: var(--ph-ratio, 4 / 5);
  width: min(760px, 100%);
  background-color: #1E1E1E;
}
.lightbox__stage .ph__label,
.lightbox__stage .ph__meta { color: var(--dh-invert-70); }

.lightbox__close,
.lightbox__nav {
  position: absolute;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  color: var(--dh-invert);
  transition: opacity var(--dur) var(--ease);
  opacity: 0.75;
}
.lightbox__close:hover,
.lightbox__nav:hover { opacity: 1; }
.lightbox__close { top: var(--s-4); right: var(--s-4); }
.lightbox__nav--prev { left: var(--s-3); top: 50%; transform: translateY(-50%); }
.lightbox__nav--next { right: var(--s-3); top: 50%; transform: translateY(-50%); }
.lightbox__nav:disabled { opacity: 0.2; cursor: default; }

.lightbox__count {
  position: absolute;
  bottom: var(--s-4);
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--f-body);
  font-size: 12px;
  letter-spacing: 0.1em;
  color: var(--dh-invert-70);
  font-variant-numeric: tabular-nums;
}


/* --------------------------------------------------------------------------
   BELOW THE FOLD — second CTA band, sibling rail, collection link
   -------------------------------------------------------------------------- */

.cta-band { background: var(--dh-paper); padding-block: var(--s-7); }
.cta-band__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  flex-wrap: wrap;
}
.cta-band__copy { max-width: 44ch; }
.cta-band__cta { width: min(420px, 100%); }

.sibling-rail .carousel__track {
  grid-auto-columns: calc((100% - (var(--s-5) * 2)) / 3.2);
  gap: var(--s-5);
}
@media (max-width: 1024px) {
  .sibling-rail .carousel__track { grid-auto-columns: calc((100% - var(--s-4)) / 2.2); }
}
@media (max-width: 768px) {
  .sibling-rail .carousel__track { grid-auto-columns: calc(100% / 1.3); gap: var(--s-4); }
}

/* Rail and block titles never wrap either. */
.sibling-rail ~ * .t-h2,
/* NO LONGER NOWRAP.
   Nowrap was a desktop promise made when the collection was called "Warner
   Bros." — restoring the full "Warner Bros. Doll Collection" (client, 24 Aug)
   makes "Explore Warner Bros. Doll Collection" 1454px wide, which overflows
   even a 1440px desktop. A promise that depends on the length of client-
   supplied copy is not a promise worth keeping; balanced wrapping is.
   Note the sibling rails ("More from DC Super-Villains") keep nowrap — those
   strings are short and controlled. */
.explore-block .t-h2,
.cta-band__copy .t-h2 { white-space: normal; text-wrap: balance; }
/* Same reasoning as .section-head__title — nowrap is a desktop promise, and
   at 440px "Explore Doll Haus of Dread™" simply does not fit on a line. */
@media (max-width: 768px) {
  .explore-block .t-h2,
  .cta-band__copy .t-h2 { white-space: normal; text-wrap: balance; }
}

.explore-block {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  flex-wrap: wrap;
  padding-block: var(--s-6);
  border-top: var(--hair);
}

@media (max-width: 768px) {
  /* "Explore {collection}" and its link sit as one pair, not two rows. */
  .explore-block { gap: var(--s-3); }
}

/* Give the mobile buy bar somewhere to sit without covering the footer. */
@media (max-width: 1024px) {
  body[data-page='product'] .footer { padding-bottom: calc(var(--s-5) + 70px); }
}


/* ==========================================================================
   CLIENT FEEDBACK ROUND 1 — 24 Aug 2026
   Everything below exists because of that round: the quiet missing-copy
   note, the tease tile, the About section, the social band, the sign-up
   pop-up. All grayscale, no radii — the build rules hold here too.
   ========================================================================== */

/* --------------------------------------------------------------------------
   QUIET MISSING-COPY NOTE (WB collection headline — open item 4).
   Deliberately small and grey: a reminder, not a design element. The loud
   dashed flag was removed by direction; this is its minimum-viable
   replacement so the gap is visible somewhere on screen.
   -------------------------------------------------------------------------- */
.col-note {
  margin-top: var(--s-4);
  font-family: var(--f-body);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #8A8A8A;
}

/* --------------------------------------------------------------------------
   TEASE TILE — "The Haus Is Expecting Company…"
   Sits in the property carousel where the unlaunched tiles used to be.
   Copy and countdown are Poppins, NOT Chocogom — the ellipsis is a
   period-built glyph and Chocogom's period draws nothing (§6 in HANDOFF),
   so display type here would render "The Haus Is Expecting Company" with a
   hole after it. Label bottom-left, mirroring the name position on real
   property tiles.
   -------------------------------------------------------------------------- */
.prop-tile--tease { cursor: default; }
.tease__media {
  position: relative;
  background: var(--dh-paper-deep);   /* still the backstop while the art loads */
  overflow: hidden;
}

/* THE TILE ART (client via Blake, 30 Sep 2026) — a near-black keyhole field.
   Supplied at 1080x1350, exactly the 4:5 of the box, so it fills without
   cropping at any width. object-fit is belt-and-braces for a future file
   that is not. */
.tease__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* THE TYPE INVERTS WITH IT. Everything in this tile was ink-on-paper, which
   on a near-black field would have been invisible — the single most likely
   way this change ships broken.

   The scrim question was measured, not guessed: the brightest pixel anywhere
   behind the copy, the countdown or the label is L=53 of 255, so white type
   clears roughly 15:1 and the art needs nothing laid over it. If the art is
   ever swapped for a lighter file, re-measure before trusting this. */
.tease__media .tease__copy,
.tease__media .tease__unit b { color: var(--dh-invert); }
.tease__media .tease__count,
.tease__media .tease__label  { color: var(--dh-invert-70); }
/* EVERYTHING IN THIS TILE IS DOUBLE ITS ORIGINAL SIZE (Blake, 28 Sep 2026).
   Type, the gaps between the blocks and the tile's own inset are all 2x the
   figures this rule shipped with, so the tile scales as one piece rather
   than growing type inside unchanged padding. The originals are kept in the
   comments beside each value — this is a deliberate size, not a drift, and
   the next person to touch it should know what it was doubled from.

   The tile is the same box it always was (297x372 at 390px, 525x656 at
   1440px); only its contents grew. The copy keeps its 14ch measure, which
   is in `ch` and therefore scales with the type, so the headline breaks
   across the same number of lines it always did. */
.tease__inner {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 32px;               /* was var(--s-3) 16px */
  padding: 48px;           /* was var(--s-4) 24px */
}
.tease__copy {
  font-family: var(--f-body);
  font-weight: 600;
  font-size: clamp(36px, 3.2vw, 48px);   /* was clamp(18px, 1.6vw, 24px) */
  line-height: 1.3;
  /* Set in caps, not rewritten in data.js: the string stays sentence case
     so the ellipsis and any future reuse of brand.teaser.copy are intact.
     The display face is still off-limits here — Chocogom draws nothing for
     "…" — and text-transform does not change the family. */
  text-transform: uppercase;
  color: var(--dh-ink);
  max-width: 14ch;
}
.tease__count {
  display: flex;
  gap: 32px;               /* was var(--s-3) 16px */
  font-family: var(--f-body);
  color: var(--dh-ink-70);
  font-size: 24px;         /* was 12px */
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.tease__unit b {
  display: block;
  font-weight: 600;
  font-size: clamp(40px, 4vw, 56px);     /* was clamp(20px, 2vw, 28px) */
  letter-spacing: 0;
  color: var(--dh-ink);
}
.tease__label {
  position: absolute;
  left: 48px;              /* was var(--s-4) 24px */
  bottom: 48px;            /* was var(--s-4) 24px */
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 24px;         /* was 12px */
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--dh-ink-70);
}

/* TRUE 2x ON PHONES TOO (Blake, 29 Sep 2026: "the text in the tile is 2x,
   not the tile itself. On mobile"). The 26 Sep version tapered below 480px
   because full 2x collided with "Coming Soon." at the original leading and
   inset. It fits - it just needed the leading and the chrome to give way
   instead of the type:

     line-height 1.30 -> 1.12   three lines of 36px instead of four
     inset       48   -> 20     the tile is 297px wide, not 525
     gap         32   -> 14

   Measured on the deployed page at 360 / 390 / 414 / 430 / 479: no
   overflow, no spill, and at 390 there is 20px of clear air above the
   label. The tile itself is untouched - it is still the 4:5 box the
   property tiles beside it are.

   The bottom inset reserves the label's row so the centred stack is laid
   out above it and cannot collide however the copy wraps. */
@media (max-width: 479px) {
  .tease__inner {
    gap: 10px;
    padding: 16px;
    padding-bottom: calc(16px + 24px * 1.2 + 6px);
  }
  .tease__copy   { font-size: 36px; line-height: 1.12; }
  .tease__unit b { font-size: 40px; }
  .tease__count  { font-size: 24px; gap: 10px; }
  .tease__label  { font-size: 24px; left: 16px; bottom: 16px; }
}

/* BELOW 360px IT GENUINELY DOES NOT FIT. At 320 the tile is 237x296, the
   copy runs to four lines at 36px and the block is taller than the tile it
   sits in. Only this narrow band tapers, and it is the only width where the
   text is not exactly double. */
@media (max-width: 359px) {
  .tease__inner {
    gap: min(10px, 3vw);
    padding: min(16px, 5vw);
    padding-bottom: calc(min(16px, 5vw) + min(24px, 6vw) * 1.2 + 6px);
  }
  .tease__copy   { font-size: min(36px, 8vw); }
  .tease__unit b { font-size: min(40px, 9vw); }
  .tease__count  { font-size: min(24px, 6vw); gap: min(10px, 3vw); }
  .tease__label  {
    font-size: min(24px, 6vw);
    left: min(16px, 5vw);
    bottom: min(16px, 5vw);
  }
}

/* --------------------------------------------------------------------------
   ABOUT DOLL HAUS — Haus icon slot on the side, copy beside it.
   Both are pending assets; the section is the designed home for them.
   -------------------------------------------------------------------------- */
.about { padding-block: var(--s-7, 96px); }
.about__inner {
  display: grid;
  grid-template-columns: minmax(0, 280px) minmax(0, 1fr);
  gap: var(--s-6);
  align-items: center;
}
.about__title { margin-bottom: var(--s-3); }
.about__note {
  font-family: var(--f-body);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #8A8A8A;
}
@media (max-width: 768px) {
  .about__inner { grid-template-columns: minmax(0, 1fr); gap: var(--s-4); }
  .about__mark { max-width: 200px; }
}

/* --------------------------------------------------------------------------
   SOCIAL BAND — "From @dollhaushq."
   Full-width row of square slots above the footer, every one clickable
   through to the account. The head stays Poppins: it is a handle, and
   Chocogom cannot be trusted with @ or the full stop (§6).
   -------------------------------------------------------------------------- */
/* EQUAL AIR TOP AND BOTTOM (Blake, 1 Oct 2026): "make the padding below the
   instagram section match the size of the padding above the headline."

   SUPERSEDES the 26 Aug rule, which set the bottom to --s-2 (8px) to match
   the gap BETWEEN the tiles — the band breathing under the tiles exactly as
   much as the tiles breathed between themselves. That logic held while the
   tiles were grey placeholder slots and the band read as one block of
   scaffolding. With four real photographs in it the band is a section like
   any other, and 64px over 8px left it sitting on the footer.

   One value now, so the two can never drift apart: 64px above the headline,
   64px under the tiles. */
.ig-band { padding-block: var(--s-6); }

/* SIDE MARGINS MATCH THE BOTTOM PADDING (Blake, 15 Sep 2026).
   The band sat in --gutter (24px) on the sides against an --s-2 (8px) bottom
   and an --s-2 gap between tiles — an outer margin three times the inner one.
   One token now governs all three, so the grid is inset by the same amount on
   the sides as underneath.

   Applied to the HEADLINE WRAP TOO, not just the grid: the headline was just
   left-aligned, and leaving it in the 1440px column would have started it
   ~200px right of the first tile it sits above. Both children share the
   band's inline padding so the type lines up with the tiles. */
/* INSET RAISED TO MATCH THE COLLECTION TILES (Blake, 26 Sep 2026):
   "increase the margin of From @dollhaushq to be in line with the DOLL HAUS
   OF... element." Measured before the change: the heading sat 8px from the
   edge while "DOLL HAUS OF DREAD" in the tile above sat at 40px, so the two
   pieces of display type stacked on the same screen started at different
   places.

   BOTH CHILDREN STILL MOVE TOGETHER, which is the point of the rule above
   and of the 15 Sep note: the heading is aligned to the TILES it sits over,
   not to the page column. Padding only one of them would put the headline
   back out of register with the first tile.

   The trade-off, on the record: the 15 Sep version tied the side inset to
   the 8px tile gap so the band was inset by the same amount on the sides as
   underneath. That balance is now traded for alignment with the rest of the
   page, which is the newer call.

   40px above 768 and the gutter below it are the ctile body's own values, so
   the two track each other if either is retuned. */
.ig-band > .wrap,
.ig-band__grid {
  max-width: none;
  padding-inline: var(--s-5);
}
@media (max-width: 768px) {
  .ig-band > .wrap,
  .ig-band__grid { padding-inline: var(--gutter); }
}
/* Promoted from a small Poppins label to a Chocogom section headline
   (Blake, 26 Aug 2026) — type comes from .t-h2/.section-head__title in the
   markup; the '@' and '.' inside it are body-face rescues (.at/.dt), since
   both are zero-contour glyphs in Chocogom. */
.ig-band__head {
  color: var(--dh-ink);
  margin-bottom: var(--s-4);
}
.ig-band__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-2);
}
.ig-tile { display: block; }
@media (max-width: 768px) {
  /* Two-up on phones; four 1:1 slots across 400px are postage stamps. */
  .ig-band__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --------------------------------------------------------------------------
   EMAIL SIGN-UP POP-UP (wireframe).
   Bratz-style split panel: form left, visual right. Hidden until the 30%
   scroll trigger fires; [hidden] wins over the flex display below. Sits
   above the header, below the tray, so an open tray still outranks it.
   -------------------------------------------------------------------------- */
.signup-pop {
  position: fixed;
  inset: 0;
  z-index: 955;              /* > --z-header (950), < --z-tray (960) */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
}
.signup-pop[hidden] { display: none; }
/* Deeper than the 0.55 it carried against a light panel: a dark panel needs
   more separation from the page behind it, not less. */
.signup-pop__scrim {
  position: absolute;
  inset: 0;
  background: rgba(10, 10, 10, 0.72);
}
/* DARK PANEL (Blake, 16 Sep 2026 — was --dh-paper). Same ground as the About
   and Submit pages, so the three dark surfaces on the site agree.

   Everything below is scoped to .signup-pop__panel rather than flipped on the
   components themselves: .subscribe, .consent, .btn and .agegate all still
   serve the LIGHT footer sign-up on every page. Only this instance inverts. */
.signup-pop__panel {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  width: min(880px, 100%);
  background: var(--dh-ink);
  color: var(--dh-invert);
  /* A black panel on a dark scrim needs an edge or it dissolves into it. */
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
}

/* --- the dark sign-up ---------------------------------------------------- */
.signup-pop__panel .signup-pop__sub { color: var(--dh-invert-70); }

.signup-pop__panel .subscribe input {
  border-bottom-color: var(--dh-invert);
  color: var(--dh-invert);
}
.signup-pop__panel .subscribe input::placeholder { color: var(--dh-invert-70); }
.signup-pop__panel .subscribe input:focus-visible {
  outline: 2px solid var(--dh-invert);
  outline-offset: 3px;
}
.signup-pop__panel .subscribe__note { color: var(--dh-invert); }

/* The primary action inverts with its ground: a --dh-ink button on a
   --dh-ink panel is an invisible button. */
.signup-pop__panel .btn {
  background: var(--dh-invert);
  color: var(--dh-ink);
  border-color: var(--dh-invert);
}
.signup-pop__panel .btn:hover {
  background: transparent;
  color: var(--dh-invert);
  border-color: var(--dh-invert);
}

.signup-pop__panel .consent { color: var(--dh-invert-70); }
/* accent-color is currentColor, so the tick follows the label — but the
   label is 72% white and a 72% tick reads as disabled. Pin it to full. */
.signup-pop__panel .consent__box { accent-color: var(--dh-invert); }
.signup-pop__panel .consent__link { color: var(--dh-invert); }

/* --- the age gate, which renders in front of that sign-up ---------------- */
.signup-pop__panel .agegate__prompt,
.signup-pop__panel .agegate__select { color: var(--dh-invert); }
.signup-pop__panel .agegate__select { border-bottom-color: var(--dh-invert); }
.signup-pop__panel .agegate__note,
.signup-pop__panel .agegate__blocked { color: var(--dh-invert-70); }
/* The dropdown LIST is painted by the OS, not by the select. Without this it
   inherits white-on-white in several browsers and the months are unreadable
   — the one part of a dark form that CSS on the control does not reach. */
.signup-pop__panel .agegate__select option {
  background: var(--dh-ink);
  color: var(--dh-invert);
}

/* --- the visual half ----------------------------------------------------- */
/* Placeholder fills are per-surface throughout this build (see base.css).
   #EDEDED on a black panel would read as a lightbox, not a slot. */
.signup-pop__panel .ph { background-color: #2A2A2A; }
.signup-pop__panel .ph__label,
.signup-pop__panel .ph__meta { color: var(--dh-invert-70); }
.signup-pop__body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--s-3);
  padding: var(--s-6) var(--s-5);
}
.signup-pop__title { font-family: var(--f-display); font-size: 32px; }
.signup-pop__sub {
  font-family: var(--f-body);
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  color: var(--dh-ink-70);
  max-width: 34ch;
}
.signup-pop__media { min-height: 320px; }
.signup-pop__media .ph { height: 100%; }

/* THE FILL BEHIND THE ARTWORK MUST BE THE PANEL, NOT WHITE.

   `.ph--filled` carries `background-color: var(--dh-sweep)` - #FFFFFF - which
   is right on a PDP, where product shots are cut out on a white sweep, and
   wrong here, where the panel is --dh-ink. `.ph--filled img` is object-fit:
   contain, so ANY mismatch between the slot's ratio and the file's letterboxes
   in that colour: white bars on a black panel.

   The margin is thin enough to be easy to miss and still ship. The slot
   renders 439x526 (0.8346) and the supplied file is 880x1056 (0.8333), so it
   fits by height and leaves ~0.7px down each side. Sub-pixel today, and
   entirely at the mercy of the next file somebody drops in.

   Transparent rather than ink so it tracks the panel if that ever changes.
   Same fix the tray tiles already carry. */
.signup-pop__media .ph--filled { background-color: transparent; }
.signup-pop__x {
  position: absolute;
  top: 0;
  right: 0;
  width: 44px;               /* the 44px floor applies here too */
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--dh-invert);
  z-index: 1;
}
@media (max-width: 768px) {
  .signup-pop__panel { grid-template-columns: minmax(0, 1fr); }
  /* Form first on phones; the visual is garnish there. */
  .signup-pop__media { display: none; }
}

/* ==========================================================================
   ABOUT PAGE — the dark editorial page.

   Added 15 Sep 2026 (Blake): large headlines, placeholder body paragraphs, a
   large logo, dark ground with the light logo on it.

   The DARK GROUND IS SCOPED TO <main>, not to <body>. The header and footer
   keep their normal treatment — taking the chrome dark as well would mean
   overriding the header, tray, social icons and footer for one page, and
   every one of those is shared with 11 others. This is the version that
   cannot break anything else. Say the word and the chrome follows.

   Still grayscale, still no border-radius — both qa-enforced.
   ========================================================================== */

.about {
  background: var(--dh-ink);
  color: var(--dh-invert);
}

.about__hero {
  /* TIGHTENED 30 Sep 2026, when the masthead lost two elements. It was
     --s-8 / --s-6 (144 / 64) to hold a 300px logo, a display headline and a
     flag line. With only the keyhole left, that much air pushed "The Haus"
     and the whole first paragraph below the fold on a laptop - the client's
     mock has both in the first screen. */
  /* THE KEYHOLE SITS CENTRED (client, 1 Oct 2026): "bring the 'The Haus'
     headline up so the lock is vertically centered".

     Centred on what the eye sees, not on the boxes. The header is the same
     --dh-ink as this band, so its top edge is invisible and the space above
     the keyhole reads from the bottom of the DOLL HAUS wordmark: 93px at
     every desktop width, 91px at <=640. Below it ran 150-154px to the cap
     line of "THE HAUS" - the keyhole's own 40px margin, this 40px padding
     and the first block's 64px padding-top stacked.

     The keyhole's margin is gone and this padding drops to --s-4, which
     lands the cap line 93-98px under the keyhole at every width - within 5px
     of the space above it. Measured in Chrome at 320, 390, 640, 800, 1024,
     1280 and 1440. Every block keeps its 64px padding-top, so the rhythm
     between sections is unchanged; only the masthead tightened.

     Known exception, not touched: 641-767px. The header's wordmark steps
     down at 768 but this band's padding steps at 640, so that one band reads
     ~123px above and ~94px below. It predates this change. */
  padding-block: var(--s-7) var(--s-4);
  text-align: center;
}

/* THE KEYHOLE (client via Blake, 30 Sep 2026) — "remove the DH logo and
   'Icons Obsession Artistry' line and put up a large graphic of the keyhole
   in white".

   It replaces .about__logo, which was the white DH bug: an <img> of a PNG on
   an opaque black square, lifted off its ground with mix-blend-mode: screen.
   None of that is needed here. i-key is a filled vector in the sprite that
   paints with currentColor, so it takes --dh-invert straight from this rule
   and stays crisp at any size.

   Sized off the VIEWPORT HEIGHT as well as the width. The keyhole is 1:1.59
   portrait and this is the only thing above the fold on the page, so a purely
   width-based size that looked right at 1440 pushed the first heading off a
   laptop screen. min() of the two keeps it large on a phone and stops it
   outgrowing a short window. */
.about__key {
  display: block;
  width: min(230px, 40vw, 26vh);
  margin-inline: auto;
  color: var(--dh-invert);
}
.about__key .icon {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 132.67 / 211.39;   /* the i-key viewBox */
}

/* .about__headline and .about__flag were removed 30 Sep 2026 with the markup
   they styled. The headline was "Icons Obsession Artistry", which the client
   asked to drop; the flag read "[PLACEHOLDER COPY - CLIENT TO SUPPLY]" and
   existed to stop this page going live as lorem, which it no longer is. */

.about__body { padding-block: 0 var(--s-8); }

.about__block {
  max-width: 760px;
  margin-inline: auto;
  padding-block: var(--s-6) 0;
}

.about__block-title {
  font-family: var(--f-display);
  font-size: clamp(26px, 4.2vw, 54px);
  line-height: 1.06;
  letter-spacing: 0.02em;
  color: var(--dh-invert);
  margin: 0 0 var(--s-4) 0;
}

.about__para {
  font-family: var(--f-body);
  font-size: 17px;
  line-height: 1.75;
  color: var(--dh-invert-70);
  margin: 0 0 var(--s-3) 0;
}
.about__para:last-child { margin-bottom: 0; }

@media (max-width: 640px) {
  .about__hero { padding-block: var(--s-6) var(--s-4); }
  .about__body { padding-block: 0 var(--s-6); }
  .about__para { font-size: 16px; }
}

/* --------------------------------------------------------------------------
   THE BUG IN THE EDITORIAL BAND — white, large, bottom right.

   .editorial is position:relative and its overlay is a flex column anchored
   bottom-LEFT, so this sits opposite the quote rather than joining its stack.
   Held off the corner by the same gutter the overlay uses, and pulled back at
   phone width where the quote runs closer to the edge.
   -------------------------------------------------------------------------- */
.editorial__bug {
  /* The file is white art on an opaque BLACK square; screen drops that ground
     into the band behind it. */
  mix-blend-mode: screen;
  position: absolute;
  right: var(--gutter);
  bottom: var(--s-6);
  width: clamp(92px, 13vw, 190px);
  height: auto;
  pointer-events: none;
}

@media (max-width: 640px) {
  .editorial__bug {
    width: clamp(64px, 20vw, 96px);
    bottom: var(--s-5);
  }
}


/* ==========================================================================
   SUBMIT YOUR OBSESSION — the fan-submission page (Blake, 15 Sep 2026).

   Dark ground, form centred horizontally AND vertically at every width. The
   centring uses min-height + grid place-items rather than a fixed height, so
   a long validation note or the age gate can grow the page instead of being
   clipped — the form is taller than the viewport on a phone and must scroll
   normally when it is.

   Dark is scoped to <main>, same call as the About page: the header, tray and
   footer are shared with 13 other pages and are not worth overriding for one.
   ========================================================================== */

.submit {
  background: var(--dh-ink);
  color: var(--dh-invert);
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding-block: calc(var(--anno-h) + var(--header-h) + var(--s-6)) var(--s-7);
  padding-inline: var(--gutter);
}

.submit__inner {
  width: 100%;
  max-width: 560px;
  text-align: center;
}

.submit__headline {
  font-family: var(--f-display);
  font-size: clamp(32px, 5.6vw, 62px);
  line-height: 1.04;
  letter-spacing: 0.02em;         /* Chocogom tracking stays positive */
  color: var(--dh-invert);
  margin: 0;
}

.submit__sub {
  margin-top: var(--s-3);
  font-family: var(--f-body);
  font-size: clamp(15px, 2vw, 19px);
  color: var(--dh-invert);
}

.submit__intro {
  margin-top: var(--s-3);
  font-family: var(--f-body);
  font-size: 14px;
  line-height: 1.7;
  color: var(--dh-invert-70);
  /* Even the two lines instead of leaving a long first and a short second
     (Blake, 15 Sep 2026). text-wrap: balance is the native way — the browser
     evens the ragged edge itself, so it keeps working when the copy is
     replaced. max-width keeps the balance from spreading too wide to read. */
  text-wrap: balance;
  max-width: 44ch;
  margin-inline: auto;
}
/* The headline and subtitle get the same treatment, for the same reason. */
.submit__headline,
.submit__sub { text-wrap: balance; }

/* The form is LEFT aligned inside a centred column: centred labels and
   fields read as a poster, not as something you fill in. */
.submit-form {
  display: grid;
  gap: var(--s-4);
  margin-top: var(--s-6);
  text-align: left;
}
/* display:grid beats the UA [hidden] rule — the age gate hides this form
   until its screen is answered, exactly as it does the sign-up. */
.submit-form[hidden] { display: none; }

.submit-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-4);
}

.submit-field { display: grid; gap: var(--s-2); min-width: 0; }

.submit-field__label {
  font-family: var(--f-body);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dh-invert-70);
}

.submit-field input,
.submit-field textarea {
  width: 100%;
  min-width: 0;
  padding: 10px 0;
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--dh-invert);
  border-radius: 0;
  font-family: var(--f-body);
  font-size: 15px;
  color: var(--dh-invert);
  resize: vertical;
}
.submit-field input::placeholder,
.submit-field textarea::placeholder { color: var(--dh-invert-70); }
.submit-field input:focus-visible,
.submit-field textarea:focus-visible { outline: 2px solid var(--dh-invert); outline-offset: 3px; }

/* White hairline (1 Oct 2026): an ink button with an ink border on the dark
   page had no visible edge. Hover keeps .btn's accent fill and border. */
.submit-form__go { justify-self: start; min-height: 44px; border-color: var(--dh-invert); }

.submit-form__privacy {
  font-family: var(--f-body);
  font-size: 12px;
  line-height: 1.6;
  color: var(--dh-invert-70);
}

.submit__note {
  margin-top: var(--s-4);
  font-family: var(--f-body);
  font-size: 13px;
  color: var(--dh-invert);
  min-height: 1.4em;
}

/* The age gate renders on the dark ground here, so its ink has to invert. */
.submit .agegate__prompt,
.submit .agegate__select { color: var(--dh-invert); }
.submit .agegate__select { border-bottom-color: var(--dh-invert); }
.submit .agegate__note,
.submit .agegate__blocked { color: var(--dh-invert-70); }
.submit .agegate { margin-top: var(--s-6); text-align: left; }

@media (max-width: 640px) {
  .submit { padding-block: calc(var(--anno-h) + var(--header-h) + var(--s-5)) var(--s-6); }
  .submit-row { grid-template-columns: minmax(0, 1fr); }
}


/* "From @dollhaushq" — LEFT aligned (Blake, 15 Sep 2026). It inherits centre
   from .section-head__title, which every other section wants; this one band
   is the exception, so it overrides rather than the shared class changing. */
.ig-band__head { text-align: left; }


/* ==========================================================================
   LEGAL PAGES — privacy.html, terms.html (Blake, 15 Sep 2026)

   A reading document, not an editorial surface: light ground, a standard
   header, a single measured column. Deliberately the plainest page on the
   site — a privacy policy that competes with the brand for attention is a
   privacy policy nobody finishes.
   ========================================================================== */

.legal { padding-bottom: var(--s-8); }

.legal__head {
  padding-block: var(--s-7) var(--s-6);
  border-bottom: 1px solid var(--dh-hairline);
  margin-bottom: var(--s-6);
}

.legal__eyebrow {
  font-family: var(--f-body);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--dh-ink-45);
  margin: 0 0 var(--s-3) 0;
}

.legal__title {
  font-family: var(--f-display);
  font-size: clamp(34px, 6vw, 72px);
  line-height: 1.04;
  letter-spacing: 0.02em;          /* Chocogom tracking stays positive */
  color: var(--dh-ink);
  margin: 0;
}

/* The placeholder flag — same pattern as the About page and the Warner Bros.
   headline. It is the only thing standing between lorem and a public launch,
   so it sits above the fold on every view and is not styled to be missed. */
.legal__flag {
  margin: var(--s-4) 0 0 0;
  padding: 10px 14px;
  display: inline-block;
  font-family: var(--f-body);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dh-ink-70);
  background: var(--dh-paper-deep);
}

.legal__intro {
  max-width: 68ch;
  margin: var(--s-4) 0 0 0;
  font-family: var(--f-body);
  font-size: 17px;
  line-height: 1.7;
  color: var(--dh-ink-70);
}

.legal__body { max-width: 68ch; }

.legal__section { margin-bottom: var(--s-6); }
.legal__section:last-child { margin-bottom: 0; }

.legal__section-title {
  font-family: var(--f-body);
  font-weight: 600;
  font-size: 15px;
  line-height: 1.4;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--dh-ink);
  margin: 0 0 var(--s-3) 0;
}

/* The number is set apart so a scanned page reads as an ordered document.
   Section titles are set in --f-body (Poppins), NOT Chocogom, precisely
   because they lead with a digit and a period — both are zero-contour glyphs
   in Chocogom and would print blank. No rescue needed on a body face. */
.legal__num { color: var(--dh-ink-45); }

.legal__para {
  font-family: var(--f-body);
  font-size: 16px;
  line-height: 1.75;
  color: var(--dh-ink-70);
  margin: 0 0 var(--s-3) 0;
}
.legal__para:last-child { margin-bottom: 0; }

@media (max-width: 640px) {
  .legal__head { padding-block: var(--s-6) var(--s-5); margin-bottom: var(--s-5); }
  .legal { padding-bottom: var(--s-6); }
  .legal__intro, .legal__para { font-size: 15px; }
}


/* --------------------------------------------------------------------------
   FULL-BLEED BANDS FILL THEIR BOX (21 Sep 2026)

   The global rule is object-fit: contain, which is right for a product shot
   on a sweep — the whole product has to be visible. It is wrong for a band
   that runs the width of the browser: a supplied photo would letterbox
   inside it against the placeholder fill rather than filling it.

   These three take cover instead, and are the slots that get a desktop AND
   a mobile crop (see placeholder() in chrome.js). Subjects must sit centred
   with room around them, because the edges crop as the window changes.
   -------------------------------------------------------------------------- */
.ctile__media .ph--filled img,
.col-hero__slot .ph--filled img,
.editorial__slot .ph--filled img {
  object-fit: cover;
  object-position: center;
}

/* ==========================================================================
   A FILLED SLOT IS THE PHOTOGRAPH — NO INSET
   (Blake, 26 Sep 2026: "these should be filling the frame, before the hover
   even.")

   base.css already says `.ph--filled { padding: 0 }`. Three rules were
   beating it on specificity, and every one of them exists to place the
   placeholder LABEL, not the image:

     #properties .prop-tile .ph   padding: var(--s-3)              (1,2,0)
     .ctile__media .ph            padding: var(--s-5) var(--gutter) (0,2,0)
     .editorial__slot .ph         padding: var(--s-5) var(--gutter) (0,2,0)

   Measured on the deployed page before the fix: the editorial band was a
   1440x720 box holding a 1344x640 image — 48px of dead ground down each
   side and 40px top and bottom, with the same on both homepage category
   tiles and the property tiles. object-fit: cover was doing its job the
   whole time; it was covering a content box that had been inset for text.

   Matched specificity, and placed at the end of the file so source order
   settles the (1,2,0) tie rather than luck. The tray keeps its own inset —
   `.tray-tiles .ph` is a deliberate 8px so the photos do not read as a
   white box, and it is not in this list.
   ========================================================================== */
#properties .prop-tile .ph--filled,
.ctile__media .ph--filled,
.editorial__slot .ph--filled { padding: 0; }


/* ==========================================================================
   A DARK BAND MUST STAY DARK WHILE IT FADES IN  (Blake, 26 Sep 2026)

   `html.js-reveal .reveal { opacity: 0 }` fades the ELEMENT, and an
   element's own background-color fades with it. Both dark bands carried
   `reveal` on the <section> itself, so for the whole 600ms rise the
   section's #121212 was transparent and the body's #F2F2F2 showed straight
   through — the band flashed light grey under the hero, then snapped to
   black once the photograph landed. Blake reported it under the hero; the
   collections band had the identical defect one section further down and
   is fixed in the same pass.

   The fix keeps `reveal` on the <section> — ui.js observes it there and the
   suite asserts it there — and moves only the HIDDEN STATE onto the
   children. The section now paints its black from first paint and never
   animates; its contents do the 24px rise and fade exactly as before, so
   the motion is unchanged. This is how #properties has always worked: the
   reveal sits on `.section-head` and `.carousel`, never on the band.

   Black here is var(--dh-ink) #121212, not #000: the hero directly above is
   #121212, and a true black would draw a visible seam between the two.
   ========================================================================== */

/* The collections band had no background at all — the ctile artwork covered
   it once revealed, which is why the gap only ever showed mid-animation. */
.collections { background: var(--dh-ink); }

html.js-reveal .editorial.reveal,
html.js-reveal .collections.reveal {
  opacity: 1;
  transform: none;
}

html.js-reveal .editorial.reveal > *,
html.js-reveal .collections.reveal > * {
  opacity: 0;
  transform: translateY(24px);
  transition:
    opacity var(--dur-reveal) var(--ease),
    transform var(--dur-reveal) var(--ease);
}

html.js-reveal .editorial.reveal.is-in > *,
html.js-reveal .collections.reveal.is-in > * {
  opacity: 1;
  transform: none;
}

/* Same reason as the base print rule: a one-shot capture never scrolls, so
   the observer never fires and these children would photograph blank. */
@media print {
  html.js-reveal .editorial.reveal > *,
  html.js-reveal .collections.reveal > * {
    opacity: 1 !important;
    transform: none !important;
  }
}


/* The alternate hero (index-alt.html, .hero--fx, four glass treatments and
   a dock row that switched between them) was removed 29 Sep 2026 - Blake:
   "remove this alt hero. I don't like it." It lived entirely behind
   .hero--fx and a generator script, so nothing on the live pages changed
   when it went. Recoverable from git history if it is ever wanted back. */

/* 404 (1 Oct 2026). Rides on the About page's dark masthead and block type;
   only the CTA needs anything of its own. The .btn default is ink on paper,
   so on this --dh-ink ground it inverts: white outline at rest, white fill
   on hover, the same states as the hero CTA. */
.not-found { text-align: center; }
.not-found .about__para { margin-inline: auto; max-width: 46ch; }
.not-found__cta {
  margin-top: var(--s-5);
  background: transparent;
  border-color: var(--dh-invert);
  color: var(--dh-invert);
}
.not-found__cta:hover { background: var(--dh-invert); border-color: var(--dh-invert); color: var(--dh-ink); }

