/* ==========================================================================
   DAGLINGS — Story Mode.

   The ladder, the cutscene player and the reward pick. Loaded by battle.html,
   because Story Mode is a view of the Battle tab rather than a page of its own.

   THE SAME RULE THIS SITE'S PAGE CSS ALWAYS FOLLOWS: nothing in here reaches
   inside a card. Selectors stop at the wrapper — `.dg-cut__card`, not
   `.dg-cut__card .dc-name`. The card component owns what a card looks like, on
   every page, and a story scene is not an exception to that. The only property
   set on a card here is a width, through the component's own sizing.

   AND THE SITE COMPOSITES THE PORTRAIT'S BACKGROUND. The PNGs and WebPs are
   cut-out figures with nothing behind them; the blue void, Halloran's red glow
   and Seraphine's gold halo are all drawn by the three `--frame` rules below.
   That is what lets one portrait file serve the ladder tile, the cutscene and
   the battle HUD.
   ========================================================================== */

/* HIDDEN HAS TO WIN. `#dg-battle .dg-wrap` sets `display: grid`, and a class
   selector beats the user-agent's `[hidden] { display: none }` every time — so
   setting `.hidden` on the duel wrapper did nothing and the board sat under the
   ladder, fully interactive. Scoped to this tab's two switched panes rather
   than written as a blanket `[hidden] { display: none !important }`, which
   would be a rule the rest of the site has to know about. */
#dg-battle #dg-duel[hidden],
#dg-battle #dg-story[hidden],
#dg-battle #dg-sheet[hidden] {
  display: none;
}

/* --- the mode switch ------------------------------------------------------
   Quick Duel | Story, at the top of the battle tab. */

#dg-battle #dg-modes {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 16px;
}

#dg-battle .dg-modes__row {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

#dg-battle .dg-modes__back {
  margin-inline-start: auto;
}

/* --- the ladder ----------------------------------------------------------- */

#dg-battle .dg-story__head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin: 0 0 14px;
}

#dg-battle .dg-story__title {
  margin: 0;
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 15px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
}

/* Replay is a quiet control, not a call to action — it sits to the right and
   stays out of the way of the ladder it is above. */
#dg-battle .dg-story__replay { margin-inline-start: auto; }

/* DEV. Marked in the one colour the site reserves for "this is not shipping",
   so it can never be mistaken for a real affordance. */
#dg-battle .dg-story__dev {
  border: 1px dashed rgba(242, 201, 76, 0.6);
  color: #f2c94c;
}
#dg-battle .dg-story__dev[aria-pressed="true"] { background: rgba(242, 201, 76, 0.16); }

/* DEV: the Scenes menu under each tile, and the barks preview. Same dashed
   yellow — none of it ships (js/story.js builds it only on tools/serve.py). */
#dg-battle .dg-duelist-slot { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
#dg-battle .dg-duelist-slot > .dg-duelist { flex: 1; }
#dg-battle .dg-scenes { position: relative; border: 1px dashed rgba(242, 201, 76, 0.45); font-size: 12px; }
#dg-battle .dg-scenes__toggle {
  cursor: pointer;
  padding: 4px 8px;
  color: #f2c94c;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
/* A dropdown over the tiles below, not a block that grows the grid row and
   stretches every tile beside it. */
#dg-battle .dg-scenes__list {
  position: absolute;
  z-index: 5;
  top: 100%;
  left: -1px;
  right: -1px;
  display: flex;
  flex-direction: column;
  padding: 0 4px 4px;
  border: 1px dashed rgba(242, 201, 76, 0.45);
  border-top: 0;
  background: var(--panel);
}
#dg-battle .dg-scenes__item {
  padding: 5px 6px;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
#dg-battle .dg-scenes__item:hover,
#dg-battle .dg-scenes__item:focus-visible { background: rgba(242, 201, 76, 0.12); }
#dg-battle .dg-barks {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: grid;
  place-items: center;
  padding: 24px 16px;
  background: rgba(4, 7, 12, 0.82);
}
#dg-battle .dg-barks__box { width: min(640px, 100%); max-height: 86vh; }
#dg-battle .dg-barks__box > .framed__in { display: flex; flex-direction: column; min-height: 0; max-height: 86vh; }
#dg-battle .dg-barks__head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
#dg-battle .dg-barks__title { flex: 1; margin: 0; font: 700 16px "Chakra Petch", system-ui, sans-serif; }
#dg-battle .dg-barks__scroll { overflow-y: auto; min-height: 0; overscroll-behavior: contain; }
#dg-battle .dg-barks__group { margin: 0 0 14px; }
#dg-battle .dg-barks__trigger {
  margin: 0 0 4px;
  color: #f2c94c;
  font: 600 12px ui-monospace, "Cascadia Mono", Consolas, monospace;
}
#dg-battle .dg-barks__pool { margin: 0 0 6px; padding: 0 0 0 14px; }
#dg-battle .dg-barks__line { font-size: 14px; line-height: 1.45; }

#dg-battle .dg-story__notice {
  margin: 0 0 16px;
  padding: 12px 14px;
  border-left: 2px solid var(--line);
  background: rgba(255, 255, 255, 0.03);
  color: var(--muted);
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* [hidden] LOSES TO A CLASS THAT SETS display (CLAUDE.md, "three rules that
   have each bitten"): js/story.js hides this box in the ordinary case, and the
   `display: flex` above was drawing it anyway — an empty bordered bar at the
   top of the ladder. Same weight plus the attribute, so it wins. */
#dg-battle .dg-story__notice[hidden] { display: none; }
#dg-battle .dg-story__notice strong { color: var(--ink); }
#dg-battle .dg-story__notice--warn { border-left-color: #f2c94c; }

/* THE LOANER OFFER, and it is teal rather than amber because nothing is wrong.
   This notice used to say "Connect a wallet to play" and be a blocker; it is now
   an offer of the first two duelists on the Field Guide, and an offer that looks
   like a warning reads as a problem the reader has to solve before starting. */
#dg-battle .dg-story__notice--loaner { border-left-color: var(--teal); }

#dg-battle .dg-story__done {
  margin: 20px 0 0;
  padding: 14px;
  text-align: center;
  color: var(--teal);
  font-family: "Chakra Petch", system-ui, sans-serif;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-size: 13px;
  border-top: 1px solid var(--line);
}

#dg-battle .dg-ladder {
  display: grid;
  /* Two up on a phone, filling out as there is room. The tile is deliberately
     narrow — ten of them have to read as a ladder, not as ten hero images. */
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 12px;
}

#dg-battle .dg-duelist {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 0 0 10px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: inherit;
  text-align: left;
  font: inherit;
  cursor: pointer;
  transition: border-color 0.16s, transform 0.16s, background 0.16s;
}

#dg-battle button.dg-duelist:hover,
#dg-battle button.dg-duelist:focus-visible {
  border-color: var(--teal);
  transform: translateY(-2px);
}

#dg-battle .dg-duelist.is-locked {
  cursor: default;
  opacity: 0.55;
}

/* PAST THE FIELD GUIDE'S REACH, which is a different kind of locked from "beat
   the one before" and should not look identical to it. Lighter, and its portrait
   keeps its colour — see the companion rule further down, beside the grayscale it
   has to beat. */
#dg-battle .dg-duelist.is-past-loaner { opacity: 0.78; }

#dg-battle .dg-duelist__rung {
  position: absolute;
  z-index: 2;
  top: 6px;
  left: 6px;
  min-width: 22px;
  padding: 2px 6px;
  text-align: center;
  background: rgba(7, 11, 18, 0.82);
  border: 1px solid var(--line);
  color: var(--faint);
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 11px;
  letter-spacing: 0.08em;
}

#dg-battle .dg-duelist.is-beaten .dg-duelist__rung {
  color: var(--teal);
  border-color: rgba(73, 224, 192, 0.5);
}

/* THE VOID BEHIND THE FIGURE. Three variants, one per `--frame`, drawn here
   rather than baked into the artwork — see the header. */
#dg-battle .dg-duelist__frame {
  position: relative;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  display: grid;
  place-items: end center;
  background:
    radial-gradient(120% 85% at 50% 15%, rgba(73, 224, 192, 0.16), transparent 62%),
    linear-gradient(180deg, #0a1520, #060a11);
}

/* RED AND GOLD ARE A GLOW, NOT A BOX. No void of their own and no line round
   the slot: the tile's panel shows through, and a halo sits behind the figure's
   shoulders. Each gradient's radii are sized so it reaches zero alpha INSIDE
   the slot on every side — this box is `overflow: hidden`, so a gradient still
   above zero at the box edge would be cut off there and draw the very
   rectangle this replaces. The stops ease out rather than stepping to
   transparent, so there is no ring where the glow ends either. */
#dg-battle .dg-duelist--red .dg-duelist__frame {
  background: radial-gradient(48% 28% at 50% 30%,
    rgba(255, 90, 90, 0.85), rgba(255, 90, 90, 0.62) 25%, rgba(255, 90, 90, 0.32) 50%,
    rgba(255, 90, 90, 0.10) 75%, rgba(255, 90, 90, 0) 100%);
}

#dg-battle .dg-duelist--gold .dg-duelist__frame {
  background: radial-gradient(48% 28% at 50% 30%,
    rgba(242, 201, 76, 0.85), rgba(242, 201, 76, 0.62) 25%, rgba(242, 201, 76, 0.32) 50%,
    rgba(242, 201, 76, 0.10) 75%, rgba(242, 201, 76, 0) 100%);
}

#dg-battle .dg-duelist__face {
  width: 100%;
  height: 100%;
  /* `cover` from the top: these are full-length figures and the face is the
     part that has to survive a 3:4 crop. */
  object-fit: cover;
  object-position: 50% 0%;
  display: block;
}

#dg-battle .dg-duelist.is-locked .dg-duelist__face { filter: grayscale(1) brightness(0.6); }
/* A duelist past the loaner's reach keeps their colour. They are waiting on a
   wallet, not behind a win the player has to go and earn, and a greyed-out face
   says the second thing.

   IT HAS TO SIT AFTER THE RULE ABOVE. Both selectors are an id plus three
   classes, so they weigh the same and document order decides — putting this up
   beside the `.is-past-loaner` opacity rule (which it belongs with, visually)
   silently loses to the grayscale. Specificity ties are the third form of the
   trap CLAUDE.md records under "A CONDITIONAL AT-RULE ADDS NO SPECIFICITY". */
#dg-battle .dg-duelist.is-past-loaner .dg-duelist__face { filter: none; }

/* Seraphine's rim light: a faint gold edge that follows her silhouette. A
   drop-shadow, because it reads the PNG's alpha — a box-shadow would trace the
   image's rectangle. After the past-loaner rule and at its weight (the :not()
   counts), so a past-loaner tile keeps the rim; a locked tile does not match
   and stays grey. Halloran gets no rim: his art already carries a red edge
   light, and a second one doubles it into an outline. */
#dg-battle .dg-duelist--gold:not(.is-locked) .dg-duelist__face {
  filter: drop-shadow(0 0 1px rgba(255, 224, 140, 0.45)) drop-shadow(0 0 4px rgba(242, 201, 76, 0.2));
}

#dg-battle .dg-duelist__q {
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 44px;
  color: rgba(238, 243, 251, 0.18);
  align-self: center;
}

#dg-battle .dg-duelist__name {
  margin: 8px 10px 0;
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-weight: 700;
  font-size: 14px;
  line-height: 1.2;
}

#dg-battle .dg-duelist__title {
  margin: 1px 10px 0;
  color: var(--faint);
  font-size: 11.5px;
  line-height: 1.3;
}

#dg-battle .dg-duelist__state {
  margin: 6px 10px 0;
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--teal);
}

#dg-battle .dg-duelist__state.is-locked { color: var(--faint); }
#dg-battle .dg-duelist.is-beaten .dg-duelist__state { color: var(--muted); }

/* --- the duelist on the battle HUD ---------------------------------------- */

#dg-battle .dg-who__face {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 0%;
  background: #0a1520;
  border: 1px solid var(--line);
  flex: none;
}

/* The ring is every duelist's HUD badge, so red and gold keep it — the same
   neutral hairline as everyone else. What changes is the light: a glow behind
   the shoulders inside the ring, fading into the blue badge's own fill, and a
   soft halo outside it. Both are round (the element is a circle), so neither
   can draw a box. No rim on the figure at 34px: the img paints its own
   background, so a drop-shadow would trace the circle and be this halo again. */
#dg-battle .dg-who--red .dg-who__face {
  background: radial-gradient(circle at 50% 38%, rgba(255, 90, 90, 0.42), rgba(255, 90, 90, 0.14) 42%, #0a1520 72%);
  box-shadow: 0 0 7px 1px rgba(255, 90, 90, 0.3);
}
#dg-battle .dg-who--gold .dg-who__face {
  background: radial-gradient(circle at 50% 38%, rgba(242, 201, 76, 0.4), rgba(242, 201, 76, 0.13) 42%, #0a1520 72%);
  box-shadow: 0 0 7px 1px rgba(242, 201, 76, 0.3);
}

/* The line a duelist says mid-duel. Anchored to the rival's side of the table
   and non-interactive, so it can never sit between a finger and a card. */
#dg-battle .dg-bubble {
  position: absolute;
  z-index: 6;
  top: 46px;
  right: 10px;
  max-width: min(300px, 74%);
  padding: 9px 12px;
  background: rgba(10, 21, 32, 0.95);
  border: 1px solid rgba(73, 224, 192, 0.35);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.45;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.22s, transform 0.22s;
}

#dg-battle .dg-bubble.is-in { opacity: 1; transform: none; }

#dg-battle .dg-tiers__who {
  margin: 0 0 4px;
  display: flex;
  flex-direction: column;
}

#dg-battle .dg-tiers__who b { font-family: "Chakra Petch", system-ui, sans-serif; font-size: 15px; }
#dg-battle .dg-tiers__who small { color: var(--faint); font-size: 12px; }

/* ==========================================================================
   The cutscene player

   Full-screen, because it is a scene. Two planes: the STAGE (backgrounds,
   the opening's block, creatures, the duelist), which a slow push can move,
   and the BOX and Skip on top of it, which nothing moves — the text being
   read stays where it is.

   Staging is decided in js/scene-cues.js and arrives here as classes and
   data attributes: `data-bg`, `data-side`, `data-speaker`, `data-expression`,
   `data-motion`, `.is-push`. Everything that moves is switched off under
   reduced motion at the bottom of this file; nothing that carries meaning is.
   ========================================================================== */

body.dg-cut-open { overflow: hidden; }

.dg-cut {
  position: fixed;
  z-index: 90;
  inset: 0;
  overflow: hidden;
  background-color: #04070c;
  opacity: 0;
  transition: opacity 0.18s, background-color 0.6s;
  cursor: pointer;
  /* The stage's floor is the box's top edge; both read this. */
  --box-h: 150px;
  --gutter: max(12px, env(safe-area-inset-left, 0px));
}

.dg-cut.is-in { opacity: 1; }
.dg-cut.is-leaving { opacity: 0; }

/* --- the stage ------------------------------------------------------------ */

.dg-cut__stage {
  position: absolute;
  inset: 0;
  transform-origin: 50% 62%;
  transition: transform 7s cubic-bezier(0.25, 0.1, 0.25, 1);
}
/* A slow push on a reveal: close enough to feel, too slow to notice starting. */
.dg-cut__stage.is-push { transform: scale(1.07); }

.dg-cut__bg,
.dg-cut__still {
  position: absolute;
  inset: 0;
  background-color: transparent;
  background-image: radial-gradient(120% 80% at 50% 0%, #0b1622, #04070c 70%);
  background-size: cover;
  background-position: 50% 40%;
  transition: opacity 0.7s;
}

/* The master void behind a duelist, under a scrim and a vignette so it is a
   place rather than a picture: the figure has to stay the brightest thing. */
.dg-cut__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(90% 70% at 50% 55%, transparent 30%, rgba(3, 6, 10, 0.86) 100%),
    linear-gradient(rgba(4, 7, 12, 0.4), rgba(4, 7, 12, 0.4));
  pointer-events: none;
}
.dg-cut--captions .dg-cut__bg { background-image: none; }
.dg-cut--captions .dg-cut__bg::after { background: radial-gradient(90% 70% at 50% 45%, transparent 40%, rgba(3, 6, 10, 0.9) 100%); }

.dg-cut__still { opacity: 0; }
.dg-cut__still.is-on { opacity: 1; }
/* A still replaces the void, and sits under the chain rather than over it. */
.dg-cut.has-still .dg-cut__bg { background-image: none; }
.dg-cut.has-still .dg-cut__bg::after { background: linear-gradient(rgba(4, 7, 12, 0.35), rgba(4, 7, 12, 0.55)); }

.dg-cut__dag {
  position: absolute;
  left: 0;
  right: 0;
  top: 2%;
  width: 100%;
  opacity: 0.62;
  transition: opacity 0.9s;
}

/* Black: the chain and the void go; what stays is the creature, the title —
   and a still, if one has been made for that beat (the notebook). */
.dg-cut[data-bg="black"] .dg-cut__bg { opacity: 0; }

/* Through: the overlay thins and the ladder behind it shows. */
.dg-cut[data-bg="through"] { background-color: rgba(4, 7, 12, 0.62); }
.dg-cut[data-bg="through"] .dg-cut__bg { opacity: 0; }

/* --- the opening's block ---------------------------------------------------- */

/* Three cubes in a column, drawn the way js/dag-flow.js draws a block, sitting
   in the middle of the chain. The middle one is the one that weighs wrong. */
.dg-cut__block {
  position: absolute;
  left: 50%;
  top: calc((100% - var(--box-h)) * 0.42);
  display: grid;
  gap: 14px;
  transform: translate(-50%, -50%);
  opacity: 0;
  transition: opacity 0.5s;
  pointer-events: none;
}
.dg-cut__block.is-on { opacity: 1; }

.dg-cut__cube {
  position: relative;
  width: 26px;
  height: 26px;
  background: #3fd9c0;
  box-shadow: 0 0 10px rgba(73, 224, 192, 0.55), inset -5px -5px 0 rgba(0, 0, 0, 0.18);
}
.dg-cut__block.is-wrong .dg-cut__cube.is-odd {
  animation: dg-cut-wrong 1.1s ease-in-out infinite;
}
@keyframes dg-cut-wrong {
  0%, 100% { transform: none; background: #3fd9c0; }
  45% { transform: translateY(3px) scale(1.14); background: #e8fff9; box-shadow: 0 0 26px rgba(160, 255, 235, 0.95); }
}

/* The crack: the odd cube splits and the light comes out of the seam. The two
   outer cubes step back so the eye goes where the creature will rise. */
.dg-cut__block.is-crack .dg-cut__cube:not(.is-odd) { opacity: 0.25; transition: opacity 0.5s; }
.dg-cut__block.is-crack .dg-cut__cube.is-odd {
  background: transparent;
  box-shadow: 0 0 60px 18px rgba(120, 255, 225, 0.5);
}
.dg-cut__block.is-crack .dg-cut__cube.is-odd::before,
.dg-cut__block.is-crack .dg-cut__cube.is-odd::after {
  content: "";
  position: absolute;
  inset: 0;
  background: #3fd9c0;
  clip-path: polygon(0 0, 55% 0, 40% 100%, 0 100%);
  animation: dg-cut-split-l 0.6s cubic-bezier(0.2, 0.8, 0.3, 1) forwards;
}
.dg-cut__block.is-crack .dg-cut__cube.is-odd::after {
  clip-path: polygon(55% 0, 100% 0, 100% 100%, 40% 100%);
  animation-name: dg-cut-split-r;
}
@keyframes dg-cut-split-l { to { transform: translate(-22px, 8px) rotate(-18deg); opacity: 0.35; } }
@keyframes dg-cut-split-r { to { transform: translate(22px, 8px) rotate(18deg); opacity: 0.35; } }

/* --- a title word ------------------------------------------------------------ */

.dg-cut__title {
  position: absolute;
  left: 0;
  right: 0;
  top: calc((100% - var(--box-h)) * 0.4);
  margin: 0;
  padding-left: 0.2em;
  text-align: center;
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-weight: 700;
  font-size: clamp(30px, 8vw, 96px);
  letter-spacing: 0.5em;
  color: var(--teal);
  text-shadow: 0 0 24px rgba(73, 224, 192, 0.55);
  white-space: nowrap;
  opacity: 0;
  filter: blur(8px);
  transition: opacity 1.4s, filter 1.4s, letter-spacing 1.8s cubic-bezier(0.2, 0.8, 0.3, 1);
  pointer-events: none;
}
.dg-cut__title.is-on { opacity: 1; filter: none; letter-spacing: 0.2em; }

/* --- flickering silhouettes (the chain running wide) ----------------------- */

.dg-cut__flicker {
  position: absolute;
  inset: 0 0 var(--box-h);
  opacity: 0;
  transition: opacity 0.6s;
  pointer-events: none;
}
.dg-cut__flicker.is-on { opacity: 1; }
.dg-cut__ghost {
  position: absolute;
  left: var(--x);
  top: var(--y);
  height: clamp(54px, 13vh, 130px);
  width: auto;
  filter: brightness(0) drop-shadow(0 0 5px rgba(73, 224, 192, 0.8));
  opacity: 0;
  animation: dg-cut-ghost 2.6s ease-in-out infinite;
  animation-delay: calc(var(--k) * 0.37s);
}
@keyframes dg-cut-ghost {
  0%, 100% { opacity: 0; }
  12% { opacity: 0.7; }
  18% { opacity: 0.15; }
  26% { opacity: 0.55; }
  48% { opacity: 0; }
}

/* --- the other duelist a line names ---------------------------------------- */

.dg-cut__mention {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 56px);
  right: 4vw;
  width: clamp(84px, 12vw, 160px);
  display: grid;
  justify-items: center;
  gap: 4px;
  opacity: 0;
  transform: translateY(-10px);
  transition: opacity 0.5s, transform 0.5s;
  pointer-events: none;
}
.dg-cut__mention.is-on { opacity: 1; transform: none; }

/* The opening's nine duelists, in a row above the box, and Wren alone as a
   silhouette (js/scene-cues.js: the "portraits" and "<Name>'s silhouette"
   cues). Both are people standing in the picture, so they fade in rather than
   rise like a creature. */
.dg-cut__cast {
  position: absolute;
  left: 50%;
  bottom: calc(var(--box-h) + 16px);
  transform: translateX(-50%) translateY(8px);
  width: min(94vw, 980px);
  display: grid;
  grid-template-columns: repeat(9, 1fr);
  gap: clamp(2px, 0.8vw, 10px);
  align-items: end;
  opacity: 0;
  transition: opacity 0.7s, transform 0.7s;
  pointer-events: none;
}
.dg-cut__cast.is-on { opacity: 1; transform: translateX(-50%); }
.dg-cut__cast-face {
  width: 100%;
  height: auto;
  filter: brightness(0.8) saturate(0.8);
  -webkit-mask-image: linear-gradient(#000 78%, transparent);
  mask-image: linear-gradient(#000 78%, transparent);
}
.dg-cut__solo {
  position: absolute;
  left: 50%;
  bottom: var(--box-h);
  height: calc((100% - var(--box-h)) * 0.62);
  transform: translateX(-50%);
  opacity: 0;
  transition: opacity 0.9s;
  pointer-events: none;
}
.dg-cut__solo.is-on { opacity: 1; }
.dg-cut__solo-face {
  height: 100%;
  width: auto;
  filter: brightness(0) drop-shadow(0 0 6px rgba(73, 224, 192, 0.9)) drop-shadow(0 0 22px rgba(73, 224, 192, 0.45));
}
/* On the far side from whoever is speaking. */
.dg-cut[data-speaker="right"] .dg-cut__mention { right: auto; left: 4vw; }
.dg-cut__mention-face {
  width: 100%;
  height: auto;
  background: radial-gradient(70% 60% at 50% 60%, rgba(73, 224, 192, 0.2), transparent 72%);
  filter: brightness(0.62) saturate(0.6);
  -webkit-mask-image: linear-gradient(#000 70%, transparent);
  mask-image: linear-gradient(#000 70%, transparent);
}
.dg-cut__mention--red .dg-cut__mention-face {
  background: radial-gradient(70% 60% at 50% 60%, rgba(255, 90, 90, 0.24), transparent 72%);
}
.dg-cut__mention-name {
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  text-align: center;
  color: var(--faint);
}

/* --- the actors: the duelist and a creature --------------------------------- */

.dg-cut__actors {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: var(--box-h);
  pointer-events: none;
  /* The portrait's width, which the side swap moves across the stage. */
  --pw: min(36vw, calc((100vh - var(--box-h) - 40px) * 0.72), 500px);
}

.dg-cut__portrait {
  position: absolute;
  bottom: 0;
  left: 5vw;
  width: var(--pw);
  transition: left 0.55s cubic-bezier(0.3, 0.7, 0.2, 1), opacity 0.4s;
  animation: dg-cut-enter 0.5s cubic-bezier(0.2, 0.8, 0.3, 1) both;
}
.dg-cut__portrait.is-right { left: calc(100% - var(--pw) - 5vw); }
@keyframes dg-cut-enter {
  from { opacity: 0; translate: -28px 0; }
}

/* The glow behind the figure — the same three variants as the ladder tile,
   drawn by the page, never by the image. */
.dg-cut__portrait::before {
  content: "";
  position: absolute;
  inset: auto -10% 0;
  height: 86%;
  background: radial-gradient(60% 55% at 50% 62%, rgba(73, 224, 192, 0.22), transparent 70%);
  pointer-events: none;
}
/* RED AND GOLD: A SPOTLIGHT HALO, NO SHAPE. Seraphine used to stand in a
   gold frame (a bordered ::after); the owner asked for no rectangle at all.
   Both now get a halo centred behind the shoulders (about 16% down the figure)
   with a wider, fainter wash over the torso under it. The pseudo-element is
   grown past the figure on the top and sides so BOTH ellipses reach zero alpha
   well inside it: the box's edge is never where the light stops, so it can
   never show. Box: 120% of the figure tall (20% above it), 150% wide. */
.dg-cut__portrait--red::before,
.dg-cut__portrait--gold::before {
  inset: -20% -25% 0;
  height: auto;
}
.dg-cut__portrait--red::before {
  background:
    radial-gradient(40% 26% at 50% 30%,
      rgba(255, 90, 90, 0.3), rgba(255, 90, 90, 0.2) 25%, rgba(255, 90, 90, 0.1) 50%,
      rgba(255, 90, 90, 0.03) 75%, rgba(255, 90, 90, 0) 100%),
    radial-gradient(46% 40% at 50% 46%,
      rgba(255, 90, 90, 0.12), rgba(255, 90, 90, 0.06) 45%, rgba(255, 90, 90, 0.015) 75%, rgba(255, 90, 90, 0) 100%);
}
.dg-cut__portrait--gold::before {
  background:
    radial-gradient(40% 26% at 50% 30%,
      rgba(242, 201, 76, 0.3), rgba(242, 201, 76, 0.2) 25%, rgba(242, 201, 76, 0.1) 50%,
      rgba(242, 201, 76, 0.03) 75%, rgba(242, 201, 76, 0) 100%),
    radial-gradient(46% 40% at 50% 46%,
      rgba(242, 201, 76, 0.11), rgba(242, 201, 76, 0.055) 45%, rgba(242, 201, 76, 0.014) 75%, rgba(242, 201, 76, 0) 100%);
}
/* The glow sits UNDER the figure: the portrait is its own stacking context,
   the ::before at z-index 0 against the pose's 1. */
.dg-cut__portrait { isolation: isolate; }
.dg-cut__portrait::before { z-index: 0; }
.dg-cut__pose { z-index: 1; }
/* Seraphine's rim light, on the breath box rather than the img: the img's
   filter is the speaking/dimmed state and the pose's is the expression fake,
   and a drop-shadow here composes with both instead of overwriting either. It
   reads the cut-out's alpha, so it follows her silhouette, not a box. Nothing
   in this chain is preserve-3d, so a filter flattens nothing. No rim on
   Halloran — his art carries its own red edge light. */
.dg-cut__portrait--gold .dg-cut__breath {
  filter: drop-shadow(0 0 1.5px rgba(255, 224, 140, 0.45)) drop-shadow(0 0 6px rgba(242, 201, 76, 0.18));
}

.dg-cut__pose {
  position: relative;
  transform-origin: 50% 100%;
  transition: transform 0.5s cubic-bezier(0.3, 0.7, 0.2, 1), filter 0.4s;
}
.dg-cut__breath {
  transform-origin: 50% 100%;
  animation: dg-cut-breathe 4.2s ease-in-out infinite;
}
@keyframes dg-cut-breathe {
  50% { transform: scale(1.012, 1.018); }
}

.dg-cut__face {
  display: block;
  width: 100%;
  height: auto;
  filter: brightness(0.62) saturate(0.8);
  transition: filter 0.3s;
}
.dg-cut__portrait.is-speaking .dg-cut__face { filter: none; }
.dg-cut__portrait.is-yielding { opacity: 0.8; }
/* A shared portrait (the Colts) never moves between voices; the half that is
   not speaking dims a little instead (js/scene-cues.js, `voice`). A mask
   rather than a filter, because a filter cannot be applied to half an image. */
.dg-cut__portrait[data-voice="left"] .dg-cut__face {
  -webkit-mask-image: linear-gradient(90deg, #000 44%, rgba(0, 0, 0, 0.62) 64%);
  mask-image: linear-gradient(90deg, #000 44%, rgba(0, 0, 0, 0.62) 64%);
}
.dg-cut__portrait[data-voice="right"] .dg-cut__face {
  -webkit-mask-image: linear-gradient(270deg, #000 44%, rgba(0, 0, 0, 0.62) 64%);
  mask-image: linear-gradient(270deg, #000 44%, rgba(0, 0, 0, 0.62) 64%);
}

/* LEAN IN on a short or exclaimed line: toward the middle of the screen,
   whichever side the portrait is on. */
.dg-cut__portrait.is-left[data-motion="lean"] .dg-cut__pose { transform: translateX(3%) scale(1.045) rotate(0.8deg); }
.dg-cut__portrait.is-right[data-motion="lean"] .dg-cut__pose { transform: translateX(-3%) scale(1.045) rotate(-0.8deg); }

/* EXPRESSIONS, FAKED — only while no expression file exists for that mood
   (`has-variant` switches them off). A brightness and a tilt is crude, and it
   is the difference between a face that reacts and one that does not. */
.dg-cut__portrait:not(.has-variant)[data-expression="pleased"] .dg-cut__pose {
  transform: translateY(-1.5%) rotate(-1.4deg);
  filter: brightness(1.08) saturate(1.12);
}
.dg-cut__portrait:not(.has-variant)[data-expression="rattled"] .dg-cut__pose {
  transform: rotate(2.2deg) translateX(-1.5%);
  filter: brightness(0.92) contrast(1.05);
}
.dg-cut__portrait:not(.has-variant)[data-expression="defeated"] .dg-cut__pose {
  transform: translateY(3%) scale(0.97) rotate(1.2deg);
  filter: brightness(0.74) saturate(0.7);
}

/* SHAKE: one-shot, on a line that lands like a blow. */
.dg-cut__portrait.is-shaking { animation: dg-cut-shake 0.46s linear; }
@keyframes dg-cut-shake {
  15% { translate: -9px 0; }
  30% { translate: 8px 0; }
  45% { translate: -6px 0; }
  60% { translate: 4px 0; }
  80% { translate: -2px 0; }
}

/* The creature. Its height comes from its band (--reach, 0–1, set by the
   player from `band.max`): a 1–5 creature stands small, a 10–16 one tall. */
.dg-cut__actor {
  position: absolute;
  bottom: 0;
  left: calc(5vw + var(--pw) * 0.84);
  height: calc((100vh - var(--box-h)) * (0.24 + 0.3 * var(--reach, 0.5)));
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.4s, transform 0.45s cubic-bezier(0.2, 0.8, 0.3, 1);
}
/* Beside the duelist, on the side away from them, nearer the middle. */
.dg-cut[data-speaker="right"] .dg-cut__actor { left: auto; right: calc(5vw + var(--pw) * 0.84); }
/* In a caption scene there is no duelist, so the creature is centre stage. */
.dg-cut--captions .dg-cut__actor {
  left: 50%;
  right: auto;
  translate: -50% 0;
  bottom: 6%;
  height: calc((100vh - var(--box-h)) * (0.36 + 0.26 * var(--reach, 0.5)));
}

.dg-cut__actor.is-in { opacity: 1; transform: none; }
.dg-cut__actor.is-out { opacity: 0; transform: translateY(12px); }
.dg-cut__actor.is-in:not(.is-out) .dg-cut__creature { animation: dg-cut-idle 3.6s ease-in-out 0.5s infinite; }
@keyframes dg-cut-idle {
  50% { transform: translateY(-1.5%); }
}

.dg-cut__creature {
  display: block;
  height: 100%;
  width: auto;
  filter: drop-shadow(0 10px 18px rgba(0, 0, 0, 0.55));
  transition: filter 0.9s;
}
/* A silhouette is the real cutout gone black with a lit rim — the shape, not
   the creature — and it lights up in place when the creature is named. */
.dg-cut__actor.is-silhouette .dg-cut__creature {
  filter: brightness(0) drop-shadow(0 0 6px rgba(73, 224, 192, 0.9)) drop-shadow(0 0 22px rgba(73, 224, 192, 0.45));
}

/* Rising out of the cracked block, then settling. */
.dg-cut__actor.is-rising.is-in { animation: dg-cut-rise 1.5s cubic-bezier(0.2, 0.8, 0.3, 1) both; }
@keyframes dg-cut-rise {
  from { transform: translateY(18%) scale(0.35); opacity: 0; }
  55% { transform: translateY(-5%) scale(1.02); opacity: 1; }
  to { transform: none; opacity: 1; }
}

/* A card, where the creature has no cutout yet. A card is a card: the only
   thing set on it here is the wrapper's width (see the header). */
.dg-cut__actor.dg-cut__card { height: auto; width: min(30vw, 190px); }
.dg-cut:not(.dg-cut--captions) .dg-cut__actor.dg-cut__card { bottom: 6%; rotate: 3deg; }

/* --- the flash ------------------------------------------------------------ */

.dg-cut__flash {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: radial-gradient(60% 50% at 50% 45%, rgba(210, 255, 245, 0.9), rgba(73, 224, 192, 0.25) 60%, transparent);
  opacity: 0;
  pointer-events: none;
}
.dg-cut__flash.is-on { animation: dg-cut-flash 0.7s ease-out; }
@keyframes dg-cut-flash {
  0% { opacity: 0; }
  12% { opacity: 0.8; }
  100% { opacity: 0; }
}
.dg-cut--red .dg-cut__flash {
  background: radial-gradient(60% 50% at 50% 45%, rgba(255, 220, 210, 0.85), rgba(255, 90, 90, 0.25) 60%, transparent);
}

/* --- the dialogue box ----------------------------------------------------- */

/* A MEASURE, NOT THE SCREEN. Full-bleed at 1440 put 170 characters on a line,
   which is not a line anybody reads; the box is centred and capped. */
.dg-cut__box {
  position: absolute;
  z-index: 4;
  left: 50%;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 14px);
  transform: translateX(-50%);
  width: min(calc(100% - 2 * var(--gutter)), 860px);
  box-sizing: border-box;
  padding: 14px 18px 20px;
  min-height: calc(var(--box-h) - 28px);
  background: rgba(7, 12, 20, 0.92);
  border: 1px solid rgba(73, 224, 192, 0.28);
  border-left-width: 3px;
}
/* The accent follows the voice: the speaker's side, the player's on the right. */
.dg-cut[data-side="right"] .dg-cut__box { border-left-width: 1px; border-right-width: 3px; }
.dg-cut[data-side="right"] .dg-cut__who { text-align: right; }
.dg-cut--red .dg-cut__box { border-color: rgba(255, 106, 106, 0.32); }
.dg-cut--gold .dg-cut__box { border-color: rgba(242, 201, 76, 0.34); }

.dg-cut__who {
  margin: 0 0 6px;
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--teal);
}

.dg-cut__text {
  margin: 0;
  min-height: 3em;
  color: var(--ink);
  font-size: 17px;
  line-height: 1.55;
}

/* A caption is the narrator, not a speaker: centred, italic, no name chip —
   and no box drawn round it, because a caption belongs to the picture. */
.dg-cut.is-caption .dg-cut__box {
  background: linear-gradient(transparent, rgba(4, 7, 12, 0.72) 45%);
  border-color: transparent;
}
.dg-cut.is-caption .dg-cut__text {
  text-align: center;
  font-style: italic;
  font-size: 19px;
  color: rgba(238, 243, 251, 0.92);
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.8);
}

.dg-cut__more {
  position: absolute;
  right: 14px;
  bottom: 8px;
  color: var(--faint);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  animation: dg-cut-pulse 1.6s ease-in-out infinite;
}

@keyframes dg-cut-pulse {
  50% { opacity: 0.35; }
}

/* The player's own line. One button, because it is the one thing they do.
   `[hidden]` first, for the reason spelt out at the top of this file: a class
   rule setting `display` beats the user agent's `[hidden]`, and without this
   the empty button sat under every line of speech in the campaign. */
.dg-cut__choice[hidden] { display: none; }

.dg-cut__choice {
  display: block;
  width: 100%;
  margin: 2px 0 0;
  padding: 13px 16px;
  background: rgba(73, 224, 192, 0.1);
  border: 1px solid rgba(73, 224, 192, 0.5);
  color: var(--ink);
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 15px;
  text-align: right;
  cursor: pointer;
  transition: background 0.16s;
}

.dg-cut__choice:hover,
.dg-cut__choice:focus-visible { background: rgba(73, 224, 192, 0.2); }

.dg-cut.is-choice .dg-cut__more { display: none; }

.dg-cut__skip {
  position: absolute;
  z-index: 5;
  top: calc(env(safe-area-inset-top, 0px) + 10px);
  right: 12px;
  padding: 8px 14px;
  background: rgba(7, 12, 20, 0.8);
  border: 1px solid var(--line);
  color: var(--muted);
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
}

.dg-cut__skip:hover,
.dg-cut__skip:focus-visible { color: var(--ink); border-color: var(--teal); }

/* ==========================================================================
   "Take one." — the reward pick
   ========================================================================== */

.dg-reward {
  position: fixed;
  z-index: 95;
  inset: 0;
  display: grid;
  grid-template-rows: auto 1fr auto;
  justify-items: center;
  align-content: center;
  gap: 18px;
  padding: 24px 12px calc(env(safe-area-inset-bottom, 0px) + 24px);
  background: radial-gradient(110% 70% at 50% 30%, #0c1a26, #04070c 72%);
  opacity: 0;
  transition: opacity 0.22s;
}

.dg-reward.is-in { opacity: 1; }

.dg-reward__say {
  margin: 0;
  font-family: "Chakra Petch", system-ui, sans-serif;
  font-size: 20px;
  letter-spacing: 0.06em;
  color: var(--ink);
  text-align: center;
}

/* The fan. Each slot is rotated and lifted by its distance from the middle,
   which `--i` carries — so five cards fan and three would fan correctly too. */
.dg-reward__fan {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  align-self: center;
  width: 100%;
}

/* THE FAN HAS TO FIT. Five cards laid side by side are 155vw at 31vw each, and
   the negative margin is what brings them back under the viewport — at -5vw it
   came to 105vw and the outer two were cut off the edges of a phone. The sum to
   keep under 100 is `5 x width + 10 x margin`. */
.dg-reward__slot {
  width: min(31vw, 148px);
  margin: 0 -7vw;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  transform: rotate(calc(var(--i) * 7deg)) translateY(calc(abs(var(--i)) * 10px));
  transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.3, 1), opacity 0.3s;
}

.dg-reward__slot:hover,
.dg-reward__slot:focus-visible {
  transform: rotate(calc(var(--i) * 7deg)) translateY(calc(abs(var(--i)) * 10px - 16px));
}

.dg-reward.is-picked .dg-reward__slot { cursor: default; }
.dg-reward__slot.is-away { opacity: 0; transform: translateY(40px) scale(0.9); }

.dg-reward__slot.is-flipped {
  margin: 0;
  width: min(62vw, 220px);
  transform: none;
}

.dg-reward__slot.is-flipped:hover { transform: none; }

/* `[hidden]` first — `.pill` sets a display and would otherwise win, which is
   the same trap the duel pane and the choice button fell into. */
.dg-reward__done[hidden] { display: none; }
.dg-reward__done { align-self: end; }

/* --- a loaner win, which hands over nothing -------------------------------- */

/* Deliberately NOT the reward fan with its cards dimmed. Five face-down cards
   that never turn over is a near-miss — five withheld prizes — and that is on the
   never-build list. This is a sentence and two buttons. */
.dg-reward--loaner {
  grid-template-rows: auto auto;
  gap: 18px;
  padding: 0 24px;
}
.dg-reward__note {
  margin: 0;
  max-width: 52ch;
  text-align: center;
  font-size: 14.5px;
  color: var(--muted);
}
.dg-reward__row { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
@media (max-width: 480px) {
  .dg-reward__note { font-size: 14px; }
}

/* --- phones --------------------------------------------------------------- */

@media (max-width: 480px) {
  /* A phone stage is tall and narrow: the duelist stands larger and further
     in, the creature overlaps the far edge of them, and the box is taller
     because the same line wraps to more rows. */
  .dg-cut { --box-h: 176px; }
  .dg-cut__actors { --pw: min(74vw, calc((100vh - var(--box-h) - 60px) * 0.72)); }
  .dg-cut__portrait { left: 2vw; }
  .dg-cut__portrait.is-right { left: calc(100% - var(--pw) - 2vw); }
  .dg-cut__actor { left: auto; right: 2vw; height: calc((100vh - var(--box-h)) * (0.16 + 0.2 * var(--reach, 0.5))); }
  .dg-cut[data-speaker="right"] .dg-cut__actor { right: auto; left: 2vw; }
  .dg-cut--captions .dg-cut__actor { height: calc((100vh - var(--box-h)) * (0.26 + 0.24 * var(--reach, 0.5))); }
  .dg-cut__actor.dg-cut__card { width: 36vw; }
  .dg-cut__mention { top: calc(env(safe-area-inset-top, 0px) + 48px); width: 64px; }
  .dg-cut__mention-name { font-size: 11px; letter-spacing: 0.06em; }
  .dg-cut__text { font-size: 15px; }
  .dg-cut.is-caption .dg-cut__text { font-size: 16.5px; }
  #dg-battle .dg-ladder { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  #dg-battle .dg-bubble { max-width: 78%; top: 42px; font-size: 12.5px; }
  /* 5 x 30 - 10 x 7.5 = 75vw, with room for the rotation's corners. */
  .dg-reward__slot { width: min(30vw, 140px); margin: 0 -7.5vw; }
  .dg-reward__slot.is-flipped { width: min(66vw, 240px); }
}

/* --- reduced motion ------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  /* MOTION OFF, CONTENT INTACT. Every creature, portrait, silhouette and
     title still appears — only the travel to it is removed. The expression
     fakes keep their brightness (that is information) and lose their tilt. */
  .dg-cut__portrait { animation: none; transition: none; opacity: 1; }
  .dg-cut__portrait.is-shaking { animation: none; }
  .dg-cut__breath,
  .dg-cut__actor .dg-cut__creature,
  .dg-cut__block .dg-cut__cube,
  .dg-cut__block .dg-cut__cube::before,
  .dg-cut__block .dg-cut__cube::after,
  .dg-cut__ghost { animation: none !important; }
  .dg-cut__ghost { opacity: 0.45; }
  .dg-cut__pose,
  .dg-cut__portrait[data-motion] .dg-cut__pose { transform: none !important; transition: none; }
  .dg-cut__actor,
  .dg-cut__actor.is-rising.is-in { transition: none; animation: none; opacity: 1; transform: none; }
  .dg-cut__actor.is-out { opacity: 0; }
  .dg-cut__stage { transition: none; transform: none; }
  .dg-cut__title { transition: none; filter: none; letter-spacing: 0.2em; }
  .dg-cut__flash { display: none; }
  .dg-cut__mention,
  .dg-cut__still,
  .dg-cut__bg,
  .dg-cut__dag { transition: none; }
  .dg-cut__more { animation: none; }
  .dg-reward__slot,
  .dg-reward,
  .dg-cut,
  #dg-battle .dg-bubble,
  #dg-battle .dg-duelist { transition: none; }
  #dg-battle button.dg-duelist:hover { transform: none; }
}

/* --- the ladder's backdrop ------------------------------------------------- */

/* The Daglabs interior still, once it exists (js/story.js sets the variable
   from data/scene.json). Under a heavy scrim: the ladder is a list to read,
   and the room is only where it hangs. Absent, nothing here applies. */
#dg-battle .has-backdrop {
  background:
    linear-gradient(rgba(10, 14, 22, 0.8), rgba(10, 14, 22, 0.94)),
    var(--dg-ladder-bg) center / cover no-repeat;
}
