/* ===========================================================================
 * modals.css — Shared Hedensia modal-system (BEM)
 *
 * Loaded AFTER design-tokens.css. Builds the canonical .hd-modal* class
 * hierarchy + exception-klasse (.hd-statblock) + utility-klasser
 * (.hd-badge*, .hd-tabs*, .hd-num-*). Semantic emphasis (.hd-emphasis--*,
 * .hd-entry-name) + narrative-pattern (.hd-narrative*) lever i utilities.css
 * (flyttet i Fase 2.4a + Fase 2.5a).
 *
 * All modals migrate to use these classes per the migration plan. Legacy
 * inline-styled modals coexist during Phase 4-5 — the new classes are purely
 * additive until each modal is migrated.
 *
 * Compatibility: vanilla CSS (Safari 16.6 iPadOS 16 friendly). Uses
 * -webkit-backdrop-filter alongside backdrop-filter for Safari fallback.
 * Wrapped in @layer components (layer #4). No container queries, no :has().
 * =========================================================================== */

@layer components {

/* --- Backdrop (legacy <div>-modaler) ---
 * Behold for kompatibilitet med modaler der endnu ikke er migreret til
 * native <dialog>. Native <dialog>::backdrop håndteres separat nedenfor.
 */
.hd-modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--hd-surface-overlay);
  z-index: var(--hd-z-overlay);
  display: flex;
  /* Mobile-first (Fase 3.4d): bottom-sheet alignment som default.
   * Desktop-override under @media (min-width: 40em) nedenfor. */
  align-items: flex-end;
  justify-content: center;
  padding: 0;
  -webkit-backdrop-filter: blur(2px);  /* Safari 16.6 iPadOS prefixed first */
  backdrop-filter: blur(2px);
}
@media (min-width: 40em) {
  .hd-modal-backdrop {
    align-items: center;
    padding: var(--hd-space-4);
  }
}

/* --- Modal container ---
 * Applies to both legacy <div class="hd-modal"> AND native <dialog class="hd-modal">.
 * Native <dialog> defaults (margin:auto, border:solid, padding:0) are reset below.
 */
.hd-modal {
  background: var(--hd-surface-2);
  color: var(--hd-text-primary);
  font-family: var(--hd-font-body);
  border-radius: var(--hd-radius-lg);
  box-shadow: var(--hd-shadow-modal);
  max-height: 90vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  z-index: var(--hd-z-modal);
}

/* --- Native <dialog> reset + open-state styling --- */
dialog.hd-modal {
  border: none;
  padding: 0;
  margin: auto;
  color: var(--hd-text-primary);
}
dialog.hd-modal:not([open]) { display: none; }
dialog.hd-modal[open] {
  display: flex;            /* override <dialog>'s default display:block when open */
  flex-direction: column;
  /* Default entrance: centered pop (admin + portal-landscape). The portrait
   * portal rule below overrides this with a bottom-sheet rise. Duration uses
   * --hd-motion-* tokens, so prefers-reduced-motion (overrides.css) zeroes it. */
  animation: hd-modal-pop var(--hd-motion-fast) var(--hd-ease-page-turn);
}

/* Native top-layer backdrop pseudo-element (replaces .hd-modal-backdrop div) */
dialog.hd-modal::backdrop {
  background: var(--hd-surface-overlay);
  -webkit-backdrop-filter: blur(2px);
  backdrop-filter: blur(2px);
  animation: hd-modal-backdrop-in var(--hd-motion-normal) var(--hd-ease-page-turn);
}

/* Content-fit sizing: the modal shrinks to its content but never below a usable
 * floor nor beyond the per-size cap (or the viewport). The portrait bottom-sheet
 * rule below overrides width to full-bleed. Safari 16.6: -webkit-fit-content. */
.hd-modal--sm,
.hd-modal--md,
.hd-modal--lg,
.hd-modal--xl {
  width: -webkit-fit-content;
  width: fit-content;
  min-width: min(320px, calc(100vw - var(--hd-space-4)));
}
.hd-modal--sm { max-width: min(420px, calc(100vw - var(--hd-space-4))); }
.hd-modal--md { max-width: min(640px, calc(100vw - var(--hd-space-4))); }
.hd-modal--lg { max-width: min(880px, calc(100vw - var(--hd-space-4))); }
.hd-modal--xl { max-width: min(1120px, calc(100vw - var(--hd-space-4))); }

/* Escape-hatch: stable full-cap width for complex form modals where fit-content
 * yields jumpy/under-sized layout (e.g. eventyrere 5-tab, questionnaire-wizard,
 * workspace-management). Add alongside the size class only if a regression shows. */
.hd-modal--fill { width: 100%; }

/* --- Entrance keyframes (shared) ---
 * rise = bottom-sheet from below; pop = centered scale/fade; backdrop-in = scrim fade. */
@keyframes hd-modal-rise { from { transform: translateY(100%); } to { transform: translateY(0); } }
@keyframes hd-modal-pop  { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } }
@keyframes hd-modal-backdrop-in { from { opacity: 0; } to { opacity: 1; } }

/* Backdrop rise/fall — synchronised with the portrait-portal dialog motion so
 * the blur is fully active ONLY when the modal is at rest. Animates BOTH opacity
 * AND backdrop-filter blur so the obscuring builds up gradually (and tears
 * down gradually on close). Same easing as the dialog keyframes
 * (--hd-ease-in-out-cubic) so the two animations perceptually track each other. */
@keyframes hd-modal-backdrop-rise {
  from {
    opacity: 0;
    -webkit-backdrop-filter: blur(0px);
    backdrop-filter: blur(0px);
  }
  to {
    opacity: 1;
    -webkit-backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
    backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
  }
}
@keyframes hd-modal-backdrop-fall {
  from {
    opacity: 1;
    -webkit-backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
    backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
  }
  to {
    opacity: 0;
    -webkit-backdrop-filter: blur(0px);
    backdrop-filter: blur(0px);
  }
}
/* N3 — portal modal rises UP via its container's `bottom:` property (layout)
 * instead of a transform on the inner sheet (compositor). Animating transform
 * inside a top-layer <dialog> was empirically broken in both Chromium and
 * WebKit: the compositor mispositioned the layer proportionally to the
 * translate value (87 px viewport-overshoot in WebKit at translateY(100%);
 * 378 px at translateY(100dvh)). Measured by the compositor-probe harness
 * (Phase A/D/B/E, branch fix/modal-rollup-compositor-positioning). The
 * legacy hd-modal-roll-up keyframe is kept temporarily as a rollback anchor;
 * scheduled for removal in a follow-up PR once dialog-rise is prod-stable.
 *
 * Strategy #1, split-host variant (2026-05-28 crease-shadow refinement):
 * the rise is decomposed across TWO synchronous keyframes that share the
 * same duration + easing:
 *
 *   • hd-modal-dialog-bottom-rise   on `dialog.hd-modal`        — animates `bottom:`
 *   • hd-modal-sheet-clip-rise      on `.hd-modal__sheet`       — animates `clip-path:`
 *
 * Their geometric invariant remains:
 *
 *     bottom + clip-inset-bottom = --player-tab-bar-size   (constant)
 *
 * The split exists so `dialog::after` (the static bottom-crease overlay
 * below) can paint at viewport-fixed coords OUTSIDE the clipped region —
 * clip-path on a parent clips its pseudo-elements regardless of position:
 * fixed, so the clip MUST live on a child (sheet) for the static crease
 * pseudo to escape. */
@keyframes hd-modal-roll-up { from { transform: translateY(100%); } to { transform: translateY(0); } }

@keyframes hd-modal-dialog-bottom-rise {
  from {
    bottom: calc(
      var(--player-tab-bar-size, 0px)
      - var(--hd-modal-roll-distance, var(--hd-visual-viewport-height, 100dvh))
    );
  }
  to   { bottom: var(--player-tab-bar-size, 0px); }
}

@keyframes hd-modal-sheet-clip-rise {
  from {
    clip-path: inset(0 0 var(--hd-modal-roll-distance, var(--hd-visual-viewport-height, 100dvh)) 0);
  }
  to { clip-path: inset(0 0 0 0); }
}

/* Close animations — mirrors of the rise pair. Activated by adding the
 * .hd-modal--closing class to the dialog (set by modal-manager.js close()).
 * Same geometric invariant in reverse: bottom drops to off-screen while
 * clip-inset-bottom grows in sync, so the dialog never paints into the tab-
 * bar region during descent. animation-fill-mode: forwards (set on the
 * .hd-modal--closing rules below) holds the off-screen end-state until the
 * manager removes the dialog from the DOM at animationend. */
@keyframes hd-modal-dialog-bottom-fall {
  from { bottom: var(--player-tab-bar-size, 0px); }
  to {
    bottom: calc(
      var(--player-tab-bar-size, 0px)
      - var(--hd-modal-roll-distance, var(--hd-visual-viewport-height, 100dvh))
    );
  }
}

@keyframes hd-modal-sheet-clip-fall {
  from { clip-path: inset(0 0 0 0); }
  to {
    clip-path: inset(0 0 var(--hd-modal-roll-distance, var(--hd-visual-viewport-height, 100dvh)) 0);
  }
}

@keyframes hd-modal-dialog-right-rise {
  from {
    right: calc(
      var(--hd-modal-resting-right)
      - var(--hd-modal-roll-distance, var(--hd-visual-viewport-width, 100dvw))
    );
  }
  to { right: var(--hd-modal-resting-right); }
}

@keyframes hd-modal-dialog-right-fall {
  from { right: var(--hd-modal-resting-right); }
  to {
    right: calc(
      var(--hd-modal-resting-right)
      - var(--hd-modal-roll-distance, var(--hd-visual-viewport-width, 100dvw))
    );
  }
}

/* The landscape pair mirrors the portrait split-host technique: the dialog
 * moves its fixed right edge while the sheet reveals from that same edge.
 * The invariant `right + clip-inset-right = --hd-modal-resting-right` keeps
 * the native dialog's top-layer content out of the right-edge tab bar. */
@keyframes hd-modal-sheet-side-clip-rise {
  from {
    clip-path: inset(0 var(--hd-modal-roll-distance, var(--hd-visual-viewport-width, 100dvw)) 0 0);
  }
  to { clip-path: inset(0 0 0 0); }
}

@keyframes hd-modal-sheet-side-clip-fall {
  from { clip-path: inset(0 0 0 0); }
  to {
    clip-path: inset(0 var(--hd-modal-roll-distance, var(--hd-visual-viewport-width, 100dvw)) 0 0);
  }
}

@keyframes hd-modal-side-crease-rise {
  from { right: var(--hd-modal-roll-distance, var(--hd-visual-viewport-width, 100dvw)); }
  to { right: 0; }
}

@keyframes hd-modal-side-crease-fall {
  from { right: 0; }
  to { right: var(--hd-modal-roll-distance, var(--hd-visual-viewport-width, 100dvw)); }
}

/* Bottom-sheet variant for player-facing modals (spell-popup, daily-prep) on mobile */
.hd-modal--bottom-sheet {
  width: 100%;
  max-width: none;
  border-radius: var(--hd-radius-lg) var(--hd-radius-lg) 0 0;
  margin-top: auto;        /* push to bottom in flex backdrop */
  max-height: 85vh;
}

/* Shared player/public modal shell. Sizing follows the .hd-modal--* content-fit
 * caps; placement (bottom-sheet vs centered) is driven by menu position in the
 * portal — see the body-mode rule below. */
dialog.hd-modal.hd-player-modal {
  max-height: 90dvh;
}

dialog.hd-modal.hd-player-modal .hd-modal__header {
  flex-shrink: 0;
}

dialog.hd-modal.hd-player-modal .hd-modal__body {
  -webkit-overflow-scrolling: touch;
}

/* Content wrapper (ModalManager). Default = display:contents: it generates no
 * box, so header/body/footer remain flex items of the dialog and admin/desktop/
 * landscape layout is byte-identical. The portrait portal rule below promotes it
 * to a real box that rolls up out of the bottom reel. */
.hd-modal__sheet { display: contents; }

/* ===========================================================================
 * Menu-position-driven placement (player + public portal only).
 * player-mode-controller.js sets body.hd-player-mode-{player-portal,anonymous}
 * on index.php. The tab-bar (player-tab-bar.css) sits at the BOTTOM in portrait
 * and on the RIGHT SIDE in landscape, switching at (min-aspect-ratio: 1/1).
 * Modals mirror that: portrait (menu at bottom) → sheet rising from below;
 * landscape (menu on side) → the same native dialog sheet pulled from behind
 * the right-edge tab bar. Both orientations keep the top bar and tab bar lit.
 * Admin pages never get these body classes → admin modals stay centered. The
 * faelles/TV mode has no tab-bar and is intentionally excluded → stays centered.
 *
 * Containment: portal modals sit WITHIN the bars (between the fixed top-bar and
 * tab-bar), like the PiP-fullscreen element — they must not cover or dim either
 * bar. The dialog box is anchored above the tab-bar and capped so it never rises
 * under the top-bar; the ::backdrop scrim is inset to the same content area so
 * both bars stay lit. (Native <dialog> top-layer paints above the bars' z-index,
 * so containment is achieved by simply not overlapping the bar regions.)
 * =========================================================================== */
@media (max-aspect-ratio: 1/1) {
  body.hd-player-mode-player-portal dialog.hd-modal,
  body.hd-player-mode-anonymous dialog.hd-modal {
    position: fixed;
    --hd-modal-side-gap: clamp(7px, 1.75vw, 14px);
    left: calc(var(--hd-visual-viewport-offset-left, 0px) + var(--hd-modal-side-gap));
    right: calc(100vw - var(--hd-visual-viewport-offset-left, 0px) - var(--hd-visual-viewport-width, 100vw) + var(--hd-modal-side-gap));
    bottom: var(--player-tab-bar-size, 0px);   /* rise out of the tab-bar, not over it */
    top: auto;
    margin: 0;
    width: auto;
    min-width: 0;
    max-width: none;
    /* Dialog max-height ends one crease-height below the top-bar boundary. The
     * sheet stays flush inside this box so its first visible parchment edge
     * emerges from the bottom-roll boundary. */
    max-height: calc(
      var(--hd-visual-viewport-height, 100vh)
      - var(--player-top-bar-height, 0px)
      - var(--player-tab-bar-size, 0px)
      - var(--hd-roller-crease-h)
    );
    border-radius: 0;
    /* Strategy #1 split-host (see keyframes above): dialog animates ONLY its
     * `bottom:` — the clip-path animation is hosted by the inner .hd-modal__
     * sheet so that `dialog::after` (static bottom-crease overlay below) can
     * paint at viewport-fixed coords outside the sheet's clipped region. */
    animation: hd-modal-dialog-bottom-rise var(--hd-motion-roll-up) var(--hd-ease-in-out-cubic);
    animation-fill-mode: backwards;
  }

  /* Close animation. modal-manager.js sets .hd-modal--closing when close() is
   * triggered (X-btn, backdrop, ESC, or tab-bar tap) and waits for animationend
   * before removing the dialog from the DOM. animation-fill-mode: forwards
   * holds the off-screen end-state across the brief gap between animationend
   * and removeChild() — without it the dialog snaps back to its rest position
   * for one frame, which reads as a flicker. */
  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--closing,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--closing {
    animation: hd-modal-dialog-bottom-fall var(--hd-motion-roll-down) var(--hd-ease-in-out-cubic);
    animation-fill-mode: forwards;
  }

  /* The box carries NO parchment + NO drop-shadow here: the parchment lives on
   * the rolling sheet, and a top-layer drop-shadow would fall straight onto the
   * bottom menu/reel below (the nav-panels are flat, so the modal must be too).
   * Guarded by :has(.hd-modal__sheet) so a sheet-less <dialog> is untouched. */
  body.hd-player-mode-player-portal dialog.hd-modal:has(.hd-modal__sheet),
  body.hd-player-mode-anonymous dialog.hd-modal:has(.hd-modal__sheet) {
    background: transparent;
    box-shadow: none;
  }

  /* The sheet (parchment + header/body/footer) is a static flex column inside
   * the dialog box. Strategy #1 split-host: the OUTER dialog animates its
   * `bottom:` and the sheet animates its OWN `clip-path:`. The sheet does NOT
   * translate or move structurally — it rides up because the dialog moves; the
   * clip merely hides the portion of the sheet that visually overlaps the tab-
   * bar region during the rise. No transform anywhere → no top-layer
   * compositor mispositioning. */
  body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__sheet,
  body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__sheet {
    position: relative;
    box-sizing: border-box;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    /* Definite max-height (NOT max-height:100% / min-height:0) so the body's
     * flex:1 1 auto scroll chain works in WebKit — a percentage/min-height-0
     * here collapses the dialog to ~0 height on Safari (same trap the base
     * .hd-modal__body comment describes). Mirrors the dialog's cap exactly,
     * including the one-crease top clearance; no transparent top/bottom margin
     * bands in the sheet layout. */
    max-height: calc(
      var(--hd-visual-viewport-height, 100vh)
      - var(--player-top-bar-height, 0px)
      - var(--player-tab-bar-size, 0px)
      - var(--hd-roller-crease-h)
    );
    background-color: var(--hd-surface-1);
    border: 1px solid rgba(94, 69, 38, 0.4);
    border-radius: 0;
    /* Strategy #1 split-host (see keyframes hd-modal-sheet-clip-rise / -fall
     * above): sheet animates its clip-path; dialog above animates its bottom.
     * Both keyframes share identical duration + ease so the invariant
     *     bottom + clip-inset-bottom = --player-tab-bar-size
     * holds frame-for-frame. Rest-state inset(0 0 0 0) is a no-op cap that
     * keeps clip-path non-`none` so re-runs interpolate from a defined source
     * (avoids the discrete `none ↔ inset()` jump). */
    animation: hd-modal-sheet-clip-rise var(--hd-motion-roll-up) var(--hd-ease-in-out-cubic);
    animation-fill-mode: backwards;
    clip-path: inset(0 0 0 0);
  }

  /* Same paper stack as the front-page portal cards, but with only the top
   * gold seam. The bottom seam is omitted so the lower edge stays governed by
   * the roll/shadow metaphor rather than reading as a framed card. */
  :root:not([data-theme="light"]) body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__sheet,
  :root:not([data-theme="light"]) body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__sheet {
    background-image:
      var(--hd-texture-gold-seam),
      linear-gradient(rgba(0,0,0,0.65), rgba(0,0,0,0.65)),
      var(--hd-texture-dark-paper);
    background-size: 60px 3px, cover, 500px 500px;
    background-repeat: repeat-x, no-repeat, repeat;
    background-position: 0 0, center, center;
  }

  [data-theme="light"] body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__sheet,
  [data-theme="light"] body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__sheet {
    background-color: transparent;
    background-image:
      var(--hd-texture-gold-seam),
      var(--hd-texture-seam-paper);
    background-size: 60px 3px, 500px 500px;
    background-repeat: repeat-x, repeat;
    background-position: 0 0, center;
  }

  body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__footer,
  body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__footer {
    background: transparent;
  }

  body.hd-player-mode-player-portal dialog.hd-modal::after,
  body.hd-player-mode-anonymous dialog.hd-modal::after {
    content: '';
    position: fixed;
    left: calc(var(--hd-visual-viewport-offset-left, 0px) + var(--hd-modal-side-gap, 0px));
    right: calc(100vw - var(--hd-visual-viewport-offset-left, 0px) - var(--hd-visual-viewport-width, 100vw) + var(--hd-modal-side-gap, 0px));
    bottom: var(--player-tab-bar-size, 0px);
    height: var(--hd-roller-crease-h);
    pointer-events: none;
    z-index: 20;
    background: var(--hd-roller-crease-up);
  }

  /* Close-animation mirror for the sheet's clip — runs in step with the
   * dialog's bottom-fall. */
  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--closing .hd-modal__sheet,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--closing .hd-modal__sheet {
    animation: hd-modal-sheet-clip-fall var(--hd-motion-roll-down) var(--hd-ease-in-out-cubic);
    animation-fill-mode: forwards;
  }

  /* The bottom boundary shadow is a viewport-static overlay above the rolling
   * sheet. The sheet itself stays flat; there is no corresponding top shadow.
   * The top-bar crease is handled by the blur below, not by a modal shadow. */

  /* Backdrop reaches the bar boundaries so blur covers the top-bar crease and
   * the bottom boundary area. The old parchment-coloured animated wash is now reduced
   * to a near-transparent blur wash: the background context is obscured by
   * backdrop-filter rather than by a visible layer placed in front of it.
   *
   * Animation overrides the default hd-modal-backdrop-in (200ms snap to full
   * opacity at constant 2px blur) with a synchronised ramp that matches the
   * dialog rise: opacity 0->1 AND blur 0px->var(--hd-modal-backdrop-blur)
   * over --hd-motion-roll-up with the same ease-in-out-cubic the dialog uses. */
  body.hd-player-mode-player-portal dialog.hd-modal::backdrop,
  body.hd-player-mode-anonymous dialog.hd-modal::backdrop {
    --hd-modal-backdrop-blur: 7px;
    --hd-modal-backdrop-blur-wash: rgba(253, 241, 220, 0.08);
    top: calc(var(--hd-visual-viewport-offset-top, 0px)
              + var(--player-top-bar-height, 0px));
    left: var(--hd-visual-viewport-offset-left, 0px);
    right: calc(100vw - var(--hd-visual-viewport-offset-left, 0px) - var(--hd-visual-viewport-width, 100vw));
    bottom: var(--player-tab-bar-size, 0px);
    background: var(--hd-modal-backdrop-blur-wash);
    -webkit-backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
    backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
    animation: hd-modal-backdrop-rise var(--hd-motion-roll-up) var(--hd-ease-in-out-cubic);
  }

  /* Mirror on close. .hd-modal--closing is set by modal-manager.js for the
   * roll-down sequence; the backdrop tears down in step with the dialog so
   * the scrim is no longer at full opacity once the dialog has dropped off-
   * screen. animation-fill-mode: forwards holds opacity=0 across the brief
   * gap between animationend and removeChild (prevents a flash of full-dim
   * backdrop before the dialog leaves the top-layer). */
  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--closing::backdrop,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--closing::backdrop {
    animation: hd-modal-backdrop-fall var(--hd-motion-roll-down) var(--hd-ease-in-out-cubic);
    animation-fill-mode: forwards;
  }
}

@media (min-aspect-ratio: 1/1) {
  body.hd-player-mode-player-portal dialog.hd-modal,
  body.hd-player-mode-anonymous dialog.hd-modal {
    --hd-modal-side-gap: clamp(7px, 1.75vw, 14px);
    --hd-modal-resting-right: calc(
      100vw
      - var(--hd-visual-viewport-offset-left, 0px)
      - var(--hd-visual-viewport-width, 100vw)
      + var(--player-tab-bar-size, 0px)
    );
    position: fixed;
    top: calc(var(--hd-visual-viewport-offset-top, 0px) + var(--player-top-bar-height, 0px) + var(--hd-modal-side-gap));
    right: var(--hd-modal-resting-right);
    bottom: var(--hd-modal-side-gap);
    left: auto;
    margin: 0;
    width: min(640px, calc(var(--hd-player-usable-width, 100vw) - var(--player-tab-bar-size, 0px) - var(--hd-space-4)));
    min-width: min(320px, calc(var(--hd-player-usable-width, 100vw) - var(--player-tab-bar-size, 0px) - var(--hd-space-4)));
    max-width: calc(var(--hd-player-usable-width, 100vw) - var(--player-tab-bar-size, 0px) - var(--hd-space-4));
    height: calc(
      var(--hd-visual-viewport-height, 100vh)
      - var(--player-top-bar-height, 0px)
      - (var(--hd-modal-side-gap) * 2)
    );
    max-height: calc(
      var(--hd-visual-viewport-height, 100vh)
      - var(--player-top-bar-height, 0px)
      - (var(--hd-modal-side-gap) * 2)
    );
    overflow: hidden;
    border-radius: 0;
    animation: hd-modal-dialog-right-rise var(--hd-motion-roll-up) var(--hd-ease-in-out-cubic);
    animation-fill-mode: backwards;
  }

  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--fill,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--fill,
  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--xl,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--xl {
    width: calc(var(--hd-player-usable-width, 100vw) - var(--player-tab-bar-size, 0px) - var(--hd-space-4));
  }

  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--closing,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--closing {
    animation: hd-modal-dialog-right-fall var(--hd-motion-roll-down) var(--hd-ease-in-out-cubic);
    animation-fill-mode: forwards;
  }

  body.hd-player-mode-player-portal dialog.hd-modal:has(.hd-modal__sheet),
  body.hd-player-mode-anonymous dialog.hd-modal:has(.hd-modal__sheet) {
    background: transparent;
    box-shadow: none;
  }

  body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__sheet,
  body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__sheet {
    position: relative;
    box-sizing: border-box;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    max-height: calc(
      var(--hd-visual-viewport-height, 100vh)
      - var(--player-top-bar-height, 0px)
      - (var(--hd-modal-side-gap) * 2)
    );
    background-color: var(--hd-surface-1);
    border: 1px solid rgba(94, 69, 38, 0.4);
    border-radius: 0;
    animation: hd-modal-sheet-side-clip-rise var(--hd-motion-roll-up) var(--hd-ease-in-out-cubic);
    animation-fill-mode: backwards;
    clip-path: inset(0 0 0 0);
  }

  :root:not([data-theme="light"]) body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__sheet,
  :root:not([data-theme="light"]) body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__sheet {
    background-image:
      linear-gradient(90deg, rgba(0,0,0,0.35), transparent),
      var(--hd-texture-gold-seam),
      linear-gradient(rgba(0,0,0,0.65), rgba(0,0,0,0.65)),
      var(--hd-texture-dark-paper);
    background-size: var(--hd-roller-crease-h) 100%, 3px 60px, cover, 500px 500px;
    background-repeat: no-repeat, repeat-y, no-repeat, repeat;
    background-position: 0 0, 0 0, center, center;
  }

  [data-theme="light"] body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__sheet,
  [data-theme="light"] body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__sheet {
    background-color: transparent;
    background-image:
      var(--hd-texture-gold-seam),
      var(--hd-texture-seam-paper);
    background-size: 3px 60px, 500px 500px;
    background-repeat: repeat-y, repeat;
    background-position: 0 0, center;
  }

  body.hd-player-mode-player-portal dialog.hd-modal .hd-modal__footer,
  body.hd-player-mode-anonymous dialog.hd-modal .hd-modal__footer {
    background: transparent;
  }

  body.hd-player-mode-player-portal dialog.hd-modal::after,
  body.hd-player-mode-anonymous dialog.hd-modal::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: var(--hd-roller-crease-h);
    pointer-events: none;
    z-index: 20;
    background: linear-gradient(90deg, transparent, rgba(0,0,0,0.28));
    animation: hd-modal-side-crease-rise var(--hd-motion-roll-up) var(--hd-ease-in-out-cubic);
    animation-fill-mode: backwards;
  }

  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--closing::after,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--closing::after {
    animation: hd-modal-side-crease-fall var(--hd-motion-roll-down) var(--hd-ease-in-out-cubic);
    animation-fill-mode: forwards;
  }

  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--closing .hd-modal__sheet,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--closing .hd-modal__sheet {
    animation: hd-modal-sheet-side-clip-fall var(--hd-motion-roll-down) var(--hd-ease-in-out-cubic);
    animation-fill-mode: forwards;
  }

  body.hd-player-mode-player-portal dialog.hd-modal::backdrop,
  body.hd-player-mode-anonymous dialog.hd-modal::backdrop {
    --hd-modal-backdrop-blur: 7px;
    --hd-modal-backdrop-blur-wash: rgba(253, 241, 220, 0.08);
    top: calc(var(--hd-visual-viewport-offset-top, 0px) + var(--player-top-bar-height, 0px));
    left: var(--hd-visual-viewport-offset-left, 0px);
    right: calc(
      100vw
      - var(--hd-visual-viewport-offset-left, 0px)
      - var(--hd-visual-viewport-width, 100vw)
      + var(--player-tab-bar-size, 0px)
    );
    bottom: 0;
    background: var(--hd-modal-backdrop-blur-wash);
    -webkit-backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
    backdrop-filter: blur(var(--hd-modal-backdrop-blur, 2px));
    animation: hd-modal-backdrop-rise var(--hd-motion-roll-up) var(--hd-ease-in-out-cubic);
  }

  body.hd-player-mode-player-portal dialog.hd-modal.hd-modal--closing::backdrop,
  body.hd-player-mode-anonymous dialog.hd-modal.hd-modal--closing::backdrop {
    animation: hd-modal-backdrop-fall var(--hd-motion-roll-down) var(--hd-ease-in-out-cubic);
    animation-fill-mode: forwards;
  }
}

/* --- Header --- */
.hd-modal__header {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--hd-space-5) calc(var(--hd-space-3) + 44px + var(--hd-space-3)) var(--hd-space-4) var(--hd-space-5);
  gap: var(--hd-space-3);
  background-image: var(--hd-texture-leather-red);
  background-size: 512px 3px;
  background-repeat: repeat-x;
  background-position: 0 100%;
}

.hd-modal__title {
  font-family: var(--hd-font-display);
  font-size: var(--hd-text-h3);
  font-weight: var(--hd-weight-bold);
  line-height: var(--hd-leading-tight);
  color: var(--hd-text-primary);
  margin: 0;
  flex: 1;
  min-width: 0;
}

.hd-modal__subtitle {
  font-size: var(--hd-text-body-small);
  color: var(--hd-text-tertiary);
  margin-top: var(--hd-space-1);
}

.hd-modal__close {
  position: absolute;
  top: var(--hd-space-3);
  right: var(--hd-space-3);
  z-index: 1;
  flex: 0 0 auto;
  font-size: var(--hd-text-h4);
  font-weight: var(--hd-weight-bold);
}

/* --- Body --- */
.hd-modal__body {
  padding: var(--hd-space-5);
  overflow-y: auto;
  font-size: var(--hd-text-body);
  line-height: var(--hd-leading-normal);
  color: var(--hd-text-primary);
  /* flex-basis MUST be auto, not 0. The dialog is a flex column sized only by
   * max-height (no explicit height). With flex-basis:0 (`flex:1`) WebKit/Safari
   * does not count the body's content toward the dialog's auto height, so the
   * dialog collapses to header+footer and the body shrinks to ~one line (Chrome
   * uses the content height and works either way). flex:1 1 auto keeps "size to
   * content, cap at max-height, scroll when over" identical in both engines. */
  flex: 1 1 auto;
  min-height: 0;
}
.hd-modal__body h2 {
  font-family: var(--hd-font-display);
  font-size: var(--hd-text-h4);
  margin: var(--hd-space-5) 0 var(--hd-space-3);
  color: var(--hd-text-primary);
}
.hd-modal__body h3 {
  font-size: var(--hd-text-h4);
  margin: var(--hd-space-4) 0 var(--hd-space-2);
  color: var(--hd-text-secondary);
}
.hd-modal__body p { margin: 0 0 var(--hd-space-3); }
.hd-modal__body a { color: var(--hd-accent-primary); text-decoration: underline; }
.hd-modal__body a:focus-visible { outline: 2px solid var(--hd-focus-ring); outline-offset: 2px; }

/* --- Footer --- */
.hd-modal__footer {
  padding: var(--hd-space-4) var(--hd-space-5);
  display: flex;
  justify-content: flex-end;
  gap: var(--hd-space-3);
  background: var(--hd-surface-1);
  flex-shrink: 0;
}

/* Shared media primitive for thumbnail-sized images inside modal content.
 * Keep large content images local to their feature; this primitive is for
 * compact identity/previews such as spell, weapon, item, ability or portrait
 * thumbs. Modal types can override the CSS variables, but not the chrome. */
.hd-modal-media-thumb {
  --hd-modal-media-thumb-size: 64px;
  --hd-modal-media-thumb-width: var(--hd-modal-media-thumb-size);
  --hd-modal-media-thumb-height: var(--hd-modal-media-thumb-size);
  flex: 0 0 var(--hd-modal-media-thumb-width);
  width: var(--hd-modal-media-thumb-width);
  height: var(--hd-modal-media-thumb-height);
  max-width: 100%;
  aspect-ratio: var(--hd-modal-media-thumb-ratio, 1);
  position: relative;
  border-radius: var(--hd-modal-media-thumb-radius, var(--hd-radius-md));
  overflow: hidden;
  background: var(--hd-surface-2);
  border: 1px solid var(--hd-border-medium);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--hd-text-tertiary);
}

.hd-modal-media-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hd-modal-media-thumb-position, center);
}

.hd-modal-media-thumb--interactive {
  cursor: zoom-in;
}

.hd-modal-media-thumb--expanded {
  cursor: zoom-out;
}

.hd-modal-media-thumb--expanded img {
  object-fit: contain;
}

.hd-modal-media-thumb img.hd-modal-media-thumb__img--rotate-ccw {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: auto;
  max-width: none;
  transform: translate(-50%, -50%) rotate(-90deg) scale(var(--hd-modal-media-thumb-rotate-scale, 1));
  transform-origin: center;
}

.hd-modal-media-thumb--round {
  --hd-modal-media-thumb-radius: 999px;
}

.hd-modal-media-thumb--portrait {
  --hd-modal-media-thumb-position: center top;
}

/* ===========================================================================
 * Modal CTA-buttons er konsolideret til .hd-btn color-variants + .hd-btn--lg
 * i utilities.css (Fase 6.4, 2026-05-16). Callsites migreret samme commit.
 * =========================================================================== */

/* --- Section helpers (use inside .hd-modal__body) --- */
.hd-modal-section { margin-bottom: var(--hd-space-5); }
.hd-modal-section__label {
  display: block;
  font-size: var(--hd-text-ui-small);
  color: var(--hd-text-tertiary);
  margin-bottom: var(--hd-space-1);
}

/* --- Form-controls inside modal-body --- */
.hd-modal__body input[type="text"],
.hd-modal__body input[type="number"],
.hd-modal__body input[type="email"],
.hd-modal__body input[type="search"],
.hd-modal__body select,
.hd-modal__body textarea {
  background: var(--hd-surface-3);
  color: var(--hd-text-primary);
  border: 1px solid var(--hd-border-medium);
  border-radius: var(--hd-radius-sm);
  padding: var(--hd-space-2) var(--hd-space-3);
  font-family: var(--hd-font-ui);
  font-size: var(--hd-text-body);
  line-height: var(--hd-leading-normal);
}
.hd-modal__body input::placeholder,
.hd-modal__body textarea::placeholder { color: var(--hd-text-secondary); } /* AAA ≥7:1 i begge temaer (tertiary faldt til ~5.9:1 AA på lyst pergament) */
.hd-modal__body input:focus-visible,
.hd-modal__body select:focus-visible,
.hd-modal__body textarea:focus-visible { outline: 2px solid var(--hd-focus-ring); outline-offset: 1px; }

/* --- Mobile bottom-sheet (spell-popup-style) ---
 * Fase 3.4d: bottom-sheet alignment (align-items: flex-end + padding 0) er
 * inverteret til mobile-first base i .hd-modal-backdrop ovenfor; desktop
 * (min-width: 40em) restituterer centered modal. Bottom-sheet border-radius
 * defineres på .hd-modal--bottom-sheet selv (L81-87) og er viewport-agnostisk. */

/* ===========================================================================
 * Exception 1: D&D statblock card (parchment inside dark modal)
 * Used by spell-popup, NPC-info, monster-popup. Renders the classic D&D
 * statblock look — light parchment bg, dark-red headers, small-caps stat
 * labels — embedded inside a .hd-modal__body.
 * =========================================================================== */
.hd-statblock {
  background: var(--hd-parchment-bg);
  color: var(--hd-parchment-text);
  font-family: var(--hd-font-body);
  border-radius: var(--hd-radius-md);
  padding: var(--hd-space-5);
}
.hd-statblock__title {
  color: var(--hd-parchment-accent);
  font-family: var(--hd-font-display);
  font-size: var(--hd-text-h3);
  font-weight: var(--hd-weight-bold);
  letter-spacing: 0.05em;
  line-height: var(--hd-leading-tight);
  margin: 0 0 var(--hd-space-1);
}
.hd-statblock__subtitle {
  font-style: italic;
  color: var(--hd-parchment-meta);
  font-size: var(--hd-text-body-small);
  margin-bottom: var(--hd-space-3);
}
.hd-statblock__rule {
  background: linear-gradient(90deg, transparent, var(--hd-parchment-rule), transparent);
  height: 3px;
  opacity: 0.8;
  margin: var(--hd-space-3) 0;
}
.hd-statblock__stats {
  display: flex;
  flex-wrap: wrap;
  gap: var(--hd-space-3);
  margin: var(--hd-space-3) 0;
}
.hd-statblock__stat-label {
  font-size: var(--hd-text-ui-small);
  font-weight: var(--hd-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--hd-parchment-accent);
}
.hd-statblock__stat-value {
  font-size: var(--hd-text-ui);
  font-variant-numeric: tabular-nums;
  color: var(--hd-parchment-text);
}
.hd-statblock__section-header {
  color: var(--hd-parchment-accent);
  font-family: var(--hd-font-display);
  font-size: var(--hd-text-h4);
  font-weight: var(--hd-weight-bold);
  border-bottom: 2px solid var(--hd-parchment-rule);
  padding-bottom: var(--hd-space-1);
  margin: var(--hd-space-4) 0 var(--hd-space-2);
}

/* Exception 2 (long-form handout) absorberet i .hd-narrative + dens BEM-
 * elementer (.hd-narrative__signature, .hd-narrative blockquote) i
 * utilities.css per Fase 2.5a. Drop-cap-rule deduplikeret. */

/* ===========================================================================
 * Status/badge typography promoted to utilities.css i Fase 6.4 (2026-05-16).
 * Klasserne (.hd-badge, .hd-badge--{success,warning,error,info}) defineres
 * nu i utilities.css som global. Nye varianter: --neutral + --xs (Fase 6.4).
 * =========================================================================== */

/* ===========================================================================
 * Tab-bar (eventyrere member-modal 5-fane pattern, ARIA-compliant)
 * =========================================================================== */
.hd-tabs {
  display: flex;
  border-bottom: 1px solid var(--hd-border-subtle);
  gap: 2px;
}
.hd-tabs__tab {
  padding: var(--hd-space-3) var(--hd-space-4);
  font-size: var(--hd-text-ui);
  font-weight: var(--hd-weight-medium);
  color: var(--hd-text-tertiary);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color var(--hd-transition-fast), border-bottom-color var(--hd-transition-fast);
  /* WCAG 2.5.5: 44×44 minimum touch-target. */
  min-height: 44px;
}
.hd-tabs__tab:hover { color: var(--hd-text-secondary); }
.hd-tabs__tab[aria-selected="true"] {
  color: var(--hd-accent-primary);
  border-bottom-color: var(--hd-accent-primary);
}
.hd-tabs__tab:focus-visible { outline: 2px solid var(--hd-focus-ring); outline-offset: -2px; }

/* ===========================================================================
 * Numeric displays (HP, AC, damage, modifiers) — tabular for stat-lining
 * =========================================================================== */
.hd-num-tabular { font-variant-numeric: tabular-nums; }
.hd-num-hp      { color: var(--hd-accent-error);   font-weight: var(--hd-weight-bold); font-variant-numeric: tabular-nums; }
.hd-num-attack  { color: var(--hd-accent-primary); font-weight: var(--hd-weight-medium); font-variant-numeric: tabular-nums; }
.hd-num-damage  { color: var(--hd-text-primary);   font-weight: var(--hd-weight-medium); font-variant-numeric: tabular-nums; }

/* ===========================================================================
 * Accessibility utility: visually-hidden (screen-reader-only labels)
 * Reuses the project's .visually-hidden pattern from style.css — included
 * here so modal-system is self-contained even when style.css isn't loaded
 * (e.g., mood_settings.php).
 * =========================================================================== */
.hd-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

} /* end @layer components */
