:root {
  --bg-deep: #120e09;
  --bg-panel: #1d1710;
  --bg-panel-raised: #261e14;
  --wood: #2a2015;
  --gold: #c9a24b;
  --gold-bright: #f0cd7a;
  --gold-dim: #8a713a;
  --parchment: #f3e8ca;
  --parchment-dark: #e3d3a5;
  --ink: #2c2213;
  --ink-soft: #5a4a30;
  --danger: #9a3324;
  --danger-bright: #c94b36;
  --ok: #4f8a5b;
  --shadow: rgba(0, 0, 0, 0.55);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--bg-deep);
  color: var(--parchment);
  font-family: 'IM Fell English', Georgia, 'Times New Roman', serif;
}

[x-cloak] { display: none !important; }

a { color: inherit; text-decoration: none; }

.realm-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(ellipse at 20% 0%, rgba(201, 162, 75, 0.10), transparent 55%),
    radial-gradient(ellipse at 85% 100%, rgba(120, 40, 30, 0.14), transparent 50%),
    repeating-linear-gradient(0deg, rgba(0,0,0,0.10) 0px, rgba(0,0,0,0.10) 1px, transparent 1px, transparent 3px),
    linear-gradient(180deg, #171009 0%, #100c07 100%);
}

/* ---------- Buttons ---------- */

.btn {
  font-family: 'Cinzel', Georgia, serif;
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  padding: 0.55rem 1rem;
  border-radius: 6px;
  border: 1px solid var(--gold-dim);
  background: var(--bg-panel-raised);
  color: var(--parchment);
  cursor: pointer;
  transition: transform 0.1s ease, box-shadow 0.15s ease, background 0.15s ease;
  white-space: nowrap;
}
.btn:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 3px 10px var(--shadow); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }

.btn-ghost { background: transparent; border-color: transparent; color: var(--gold-bright); }
.btn-ghost:hover:not(:disabled) { color: var(--gold); text-decoration: underline; }

/* .btn-ghost's light gold reads fine on the dark toolbar, but every modal
   ("Cancel", "Upload image" / "Replace image") sits on the light parchment
   background instead, where that same light gold has poor contrast. */
.modal .btn-ghost { color: var(--ink-soft); }
.modal .btn-ghost:hover:not(:disabled) { color: var(--ink); }

.btn-primary {
  background: linear-gradient(180deg, var(--gold-bright), var(--gold));
  color: var(--ink);
  border-color: var(--gold);
  font-weight: 600;
}
.btn-secondary { background: var(--bg-panel-raised); border-color: var(--gold-dim); }
.btn-active {
  background: linear-gradient(180deg, #e6d8a8, var(--gold-bright));
  color: var(--ink);
  border-color: var(--gold-bright);
  box-shadow: 0 0 12px rgba(240, 205, 122, 0.6);
}
.btn-danger { background: var(--danger); border-color: var(--danger-bright); color: var(--parchment); }
.btn-danger:hover:not(:disabled) { background: var(--danger-bright); }

.btn-small { padding: 0.3rem 0.6rem; font-size: 0.75rem; }

/* ---------- Dashboard ---------- */

.dashboard { max-width: 1100px; margin: 0 auto; padding: 3rem 1.5rem 6rem; }

.dashboard-header { text-align: center; margin-bottom: 3rem; }

.realm-title {
  font-family: 'Cinzel Decorative', 'Cinzel', serif;
  font-weight: 900;
  font-size: clamp(2.2rem, 5vw, 3.4rem);
  margin: 0;
  color: var(--gold-bright);
  text-shadow: 0 2px 0 #000, 0 0 24px rgba(240, 205, 122, 0.35);
}

.realm-subtitle {
  font-style: italic;
  color: var(--parchment-dark);
  opacity: 0.85;
  margin-top: 0.4rem;
}

.scenario-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
  gap: 2.1rem;
}

.scenario-card {
  position: relative;
  background: linear-gradient(180deg, var(--parchment), var(--parchment-dark));
  color: var(--ink);
  border-radius: 15px;
  border: 1px solid #b9a061;
  padding: 2.1rem 1.8rem 1.65rem;
  box-shadow: 0 6px 18px var(--shadow), inset 0 0 30px rgba(120, 90, 40, 0.08);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
  display: block;
}
.scenario-card:hover {
  transform: translateY(-4px) rotate(-0.3deg);
  box-shadow: 0 12px 26px var(--shadow), 0 0 18px rgba(240, 205, 122, 0.25);
}

/* Cover art (uploadable, distinct from the in-canvas background) -- falls
   back to the 🗺️ emblem, same "empty" convention as every other image
   field in this app, when no cover_image has been uploaded yet. */
.scenario-card-cover {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 9px;
  margin-bottom: 0.9rem;
  background-color: rgba(0, 0, 0, 0.12);
  background-size: cover;
  background-position: center;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(0, 0, 0, 0.15);
}
.scenario-card-emblem { font-size: 2.7rem; }

.scenario-card-name {
  font-family: 'Cinzel', serif;
  font-size: 1.7rem;
  margin: 0 0 0.75rem;
  color: var(--ink);
}

.scenario-card-meta { font-size: 1.3rem; color: var(--ink-soft); }
.scenario-card-meta .dot { margin: 0 0.35rem; }

/* GM-only controls (cover upload, hide toggle, hold-to-delete) -- hidden
   entirely from players, same gating as every other management icon row
   in this app (character-icons, note-icons, ...). */
.scenario-card-gm-controls {
  position: absolute;
  top: 0.7rem;
  right: 0.8rem;
  display: flex;
  gap: 0.4rem;
}
.scenario-card-icon-btn {
  position: relative;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.2);
  background: rgba(243, 232, 202, 0.9);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.95rem;
  cursor: pointer;
  overflow: hidden;
  line-height: 1;
}
.scenario-card-icon-btn:hover { background: var(--parchment); }

.visibility-modal { width: min(440px, calc(100vw - 2rem)); }
.visibility-help {
  margin: 0.4rem 0 1rem;
  color: var(--ink-soft);
  line-height: 1.45;
}
.visibility-options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.55rem;
  max-height: min(46vh, 360px);
  overflow-y: auto;
  padding: 0.2rem;
}
.visibility-option {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  min-height: 42px;
  padding: 0.55rem 0.7rem;
  border: 1px solid rgba(81, 54, 25, 0.25);
  border-radius: 7px;
  background: rgba(255, 248, 225, 0.5);
  cursor: pointer;
}
.visibility-option:hover { background: rgba(255, 248, 225, 0.85); }
.visibility-option input { accent-color: var(--danger); }
.visibility-option small {
  margin-left: auto;
  color: var(--ink-soft);
  font-size: 0.68rem;
}
.visibility-option-locked {
  grid-column: 1 / -1;
  cursor: default;
  border-color: var(--gold-dim);
  background: rgba(240, 205, 122, 0.16);
}
.visibility-summary {
  min-height: 1.4rem;
  margin-top: 0.8rem;
  color: var(--ink-soft);
  font-size: 0.82rem;
  font-style: italic;
}
@media (max-width: 520px) {
  .visibility-options { grid-template-columns: 1fr; }
}

/* Delete: a left-to-right fill that grows while the button is held, drawn
   BEHIND the trash glyph -- see startPressDelete()/cancelPressDelete() in
   dashboard.js. Reaching full width (scaleX(1)) is what arms the "Are you
   sure?" modal; releasing early resets it to 0 with no effect at all. */
.scenario-card-delete { color: var(--ink); }
.delete-press-fill {
  position: absolute;
  inset: 0;
  background: var(--danger-bright);
  transform-origin: left;
  transform: scaleX(0);
  transition: transform 0.05s linear;
}
.scenario-card-icon-glyph { position: relative; z-index: 1; }

.confirm-delete-text { color: var(--ink-soft); font-size: 0.92rem; line-height: 1.4; margin: 0.6rem 0 1rem; }

.dashboard-gm-toggle { margin: 0; width: fit-content; }
.dashboard-account-row {
  margin: 1rem auto 0;
  width: fit-content;
  display: flex;
  align-items: center;
  gap: 1rem;
}

/* The dashboard sits in a centered, padded column (unlike the scenario
   page's full-width toolbar), so .clock-bar's full-bleed strip look needs
   a few overrides here: centered, rounded, with its own bottom margin
   instead of butting straight into the scenario grid. */
.dashboard-clock-bar {
  justify-content: center;
  margin: 0 auto 2rem;
  width: fit-content;
  border: 1px solid var(--gold-dim);
  border-radius: 10px;
  box-shadow: 0 4px 14px var(--shadow);
}

.empty-state { text-align: center; color: var(--parchment-dark); margin: 4rem 0; }
.empty-state-sub { opacity: 0.7; font-style: italic; }

.fab-create {
  position: fixed;
  right: 2rem;
  bottom: 2rem;
  padding: 0.9rem 1.4rem;
  border-radius: 999px;
  border: 1px solid var(--gold-bright);
  background: linear-gradient(180deg, var(--gold-bright), var(--gold));
  color: var(--ink);
  font-family: 'Cinzel', serif;
  font-weight: 600;
  font-size: 0.95rem;
  cursor: pointer;
  box-shadow: 0 8px 20px var(--shadow);
}
.fab-create:hover { transform: translateY(-2px); box-shadow: 0 10px 26px var(--shadow); }
.fab-plus { font-size: 1.1rem; margin-right: 0.2rem; }

/* ---------- Modals (shared) ---------- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(8, 6, 3, 0.72);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: 1rem;
}

.modal {
  background: linear-gradient(180deg, var(--parchment), var(--parchment-dark));
  color: var(--ink);
  border-radius: 12px;
  border: 1px solid #b9a061;
  box-shadow: 0 20px 50px rgba(0,0,0,0.6);
  padding: 1.6rem;
  max-width: 520px;
  width: 100%;
  /* Added once the Relation picker's sub-type grid grew to 11 options
     (Authority) -- long enough on a short/narrow screen to overflow the
     viewport with no way to reach the buttons below it. Harmless for every
     shorter modal, which never gets tall enough to hit this cap. */
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
}

.modal h3 {
  font-family: 'Cinzel', serif;
  margin-top: 0;
}

.modal-create input[type="text"], .modal-create input[type="password"] {
  width: 100%;
  padding: 0.6rem 0.7rem;
  font-size: 1rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
  font-family: inherit;
}

.gm-passphrase-error { color: var(--danger); font-size: 0.85rem; margin: 0.5rem 0 0; }
.gm-secrets-lock { font-size: 0.8rem; padding: 0.45rem 0.75rem; }

/* GM Secrets panel -- see the "GM secrets access" comment above
   SECRET_MODELS in app.py. Dashed red border deliberately distinguishes
   this from every other section of these modals: it's the one thing here
   that's a REAL access boundary, not just a soft `hidden` toggle. */
.secrets-panel {
  margin-top: 0.8rem;
  padding: 0.7rem 0.8rem;
  border: 1px dashed var(--danger);
  border-radius: 8px;
  background: rgba(154, 51, 36, 0.06);
}
.secrets-heading {
  margin: 0 0 0.4rem;
  font-family: 'Cinzel', serif;
  font-size: 0.9rem;
  color: var(--danger);
}
.secrets-locked { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; font-size: 0.85rem; color: var(--ink-soft); }
.secrets-locked p { margin: 0; }
.secrets-textarea {
  width: 100%;
  min-height: 90px;
  resize: vertical;
  font-family: inherit;
  font-size: 0.9rem;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}
.secrets-actions { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.5rem; }
.secrets-saved-hint { font-size: 0.8rem; color: var(--ok); font-style: italic; }

/* Set Scale modal's distance+unit row */
.scale-modal-fields { display: flex; gap: 0.6rem; margin-top: 0.6rem; }
.scale-modal-fields input[type="number"] {
  flex: 1;
  padding: 0.6rem 0.7rem;
  font-size: 1rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
  font-family: inherit;
}
.scale-modal-fields select {
  padding: 0.6rem 0.7rem;
  font-size: 1rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
  font-family: inherit;
}

/* The scale bar: a small cartographer's-legend overlay, fixed to the
   corner of the screen (not the canvas) so it stays put while panning --
   see the comment above it in scenario.html. The striped graphic is a
   single repeating-linear-gradient rather than several divs: four dark/
   light segment pairs, the classic "barber pole" ruler look. */
.map-scale-bar {
  position: fixed;
  left: 18px;
  bottom: 18px;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  background: linear-gradient(180deg, var(--parchment), var(--parchment-dark));
  border: 1px solid #b9a061;
  border-radius: 8px;
  box-shadow: 0 4px 14px var(--shadow);
  font-family: 'Cinzel', serif;
}
.map-scale-graphic-wrap { display: flex; flex-direction: column; gap: 0.2rem; }
.map-scale-graphic {
  height: 10px;
  border: 1px solid var(--ink);
  border-radius: 2px;
  background: repeating-linear-gradient(
    90deg,
    var(--ink) 0,
    var(--ink) 12.5%,
    var(--parchment) 12.5%,
    var(--parchment) 25%
  );
}
.map-scale-labels {
  display: flex;
  justify-content: space-between;
  font-size: 0.7rem;
  color: var(--ink-soft);
}
.map-scale-clear {
  background: transparent;
  border: none;
  color: var(--ink-soft);
  cursor: pointer;
  font-size: 0.9rem;
  padding: 0 0.2rem;
  line-height: 1;
}
.map-scale-clear:hover { color: var(--danger); }

.modal-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: 1.2rem; }

.modal-note-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.modal-close { background: transparent; border: none; font-size: 1.4rem; cursor: pointer; color: var(--ink-soft); }
.modal-close:hover { color: var(--danger); }

/* "Show to All" -- Note/Character/Organization/Place modals. Sits right
   under the header, above everything else (tabs included, for the
   three tabbed modals), since it applies to the whole card regardless
   of which tab happens to be selected. Deliberately not gated by
   gmMode anywhere it's used -- open to players too, see
   handle_entity_show_to_all's comment, app.py. */
.show-to-all-row { margin: 0.6rem 0 0.2rem; }

.modal-note-image {
  width: 100%;
  height: 320px;
  border-radius: 8px;
  background: #d9caa0;
  border: 1px solid #b9a061;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  margin: 0.8rem 0;
}
.modal-note-image img { max-width: 100%; max-height: 100%; object-fit: contain; }

.modal-note-text { white-space: pre-wrap; line-height: 1.5; }

.modal-note-upload { display: flex; align-items: center; gap: 0.8rem; margin-top: 0.8rem; }
.upload-label { position: relative; overflow: hidden; }
.upload-input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.upload-status { font-style: italic; opacity: 0.7; font-size: 0.9rem; }

/* ---------- Scenario canvas page ---------- */

.scenario-app { display: flex; flex-direction: column; height: 100vh; }

.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem 1.2rem;
  background: linear-gradient(180deg, var(--wood), var(--bg-panel));
  border-bottom: 2px solid var(--gold-dim);
  flex-wrap: wrap;
}

.toolbar-left, .toolbar-right, .toolbar-actions { display: flex; align-items: center; gap: 0.8rem; flex-wrap: wrap; }
.toolbar-user { font-family: 'Cinzel', serif; font-size: 0.8rem; color: var(--parchment-dark); }
.toolbar-logout-form { margin: 0; display: contents; }
/* Wraps a <template x-if> group inside .toolbar-actions (the GM-only
   Background/Set Scale pair, and the Editor-Mode-only Add-X group) --
   display: contents makes the wrapper invisible to the flex layout, so
   its buttons sit in .toolbar-actions' own flex row with its existing
   gap, exactly as if there were no wrapper element at all. */
.toolbar-inline-group { display: contents; }

/* World Clock -- a second, separate tool row below the main toolbar (see
   the comment above .clock-bar in scenario.html/index.html). Campaign-wide,
   so date/time text is always visible; only GM-gated controls (nudges,
   Play/Pause, the two modals) mutate anything. */
.clock-bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 1.2rem;
  background: linear-gradient(180deg, var(--bg-panel), var(--bg-panel-raised));
  border-bottom: 1px solid var(--gold-dim);
  flex-wrap: wrap;
  font-family: 'Cinzel', serif;
}
.clock-date, .clock-time {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 0.3rem 0.6rem;
  color: var(--parchment);
  font-family: inherit;
  font-size: 0.95rem;
  cursor: default;
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
}
.clock-time { font-variant-numeric: tabular-nums; font-size: 1.1rem; color: var(--gold-bright); }
.clock-date-short { font-size: 0.78rem; color: var(--parchment-dark); opacity: 0.85; }

/* Red Moon phase -- see moonPhaseIconMarkup() in canvas.js/dashboard.js */
.clock-moon {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.4rem;
  color: var(--parchment-dark);
  font-size: 0.85rem;
}
.moon-phase-icon { display: block; vertical-align: middle; }
.clock-moon-label { font-style: italic; }
/* Highlighted, distinct from the rest of the clock bar's muted text --
   per the feature request ("highlights the holy day"). Can run fairly
   long (some days list 4 gods), and this row doesn't wrap -- accepted
   as a known space tradeoff rather than pre-emptively truncating. */
.clock-holy-day {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.6rem;
  color: var(--gold-bright);
  background: rgba(240, 205, 122, 0.12);
  border: 1px solid var(--gold-dim);
  border-radius: 6px;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
}
.clock-clickable { cursor: pointer; }
.clock-clickable:hover { border-color: var(--gold-dim); background: rgba(240, 205, 122, 0.08); }

.clock-nudge-group { display: flex; gap: 0.2rem; }
.clock-nudge {
  background: var(--bg-panel-raised);
  border: 1px solid var(--gold-dim);
  border-radius: 5px;
  color: var(--parchment);
  font-family: inherit;
  font-size: 0.75rem;
  padding: 0.25rem 0.4rem;
  cursor: pointer;
  line-height: 1;
}
.clock-nudge:hover { background: var(--wood); }

.clock-play-pause {
  background: var(--bg-panel-raised);
  border: 1px solid var(--gold-dim);
  border-radius: 50%;
  width: 28px;
  height: 28px;
  color: var(--gold-bright);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  line-height: 1;
}
.clock-play-pause:hover { background: var(--wood); }

.clock-bar-divider { width: 1px; align-self: stretch; background: var(--gold-dim); opacity: 0.4; margin: 0 0.2rem; }

/* Weather -- the clock-bar's read-only/click-to-edit display. See
   thermometerIconMarkup()/cloudIconMarkup()/windIconMarkup() in
   canvas.js/dashboard.js for how the icons themselves are drawn. */
.clock-weather {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 0.3rem 0.6rem;
  color: var(--parchment);
  font-family: inherit;
  cursor: default;
}
.clock-weather-item {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.82rem;
  white-space: nowrap;
}
.weather-icon { display: block; vertical-align: middle; }
.clock-weather-unset { font-size: 0.85rem; color: var(--parchment-dark); font-style: italic; opacity: 0.7; }

/* Rain -- the toolbar-level readout; see the matching template comment
   in scenario.html for why "passed" is deliberately excluded here. Sits
   between Weather and Party Leader, grouped visually with Weather (no
   divider between them) since it's really the same forecast, just its
   own line item. */
.clock-rain {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.82rem;
  color: var(--parchment);
  white-space: nowrap;
}

/* Set Weather modal */
.weather-modal { max-width: 620px; }
.weather-modal-columns { display: flex; gap: 1.2rem; margin: 0.8rem 0; }
.weather-modal-column { flex: 1; display: flex; flex-direction: column; gap: 0.4rem; }
.weather-modal-heading {
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin: 0 0 0.2rem;
  border-bottom: 1px solid var(--ink-soft);
  padding-bottom: 0.2rem;
}
.weather-option {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--ink);
  cursor: pointer;
  padding: 0.15rem 0;
}
.weather-option-disabled { opacity: 0.4; cursor: default; }
.weather-option-note { font-size: 0.7rem; font-style: italic; color: var(--ink-soft); }

/* Rain -- see weather:roll_rain's own comment, app.py, for the full
   randomizer design this section exposes. Sits below the three-column
   Warmth/Cloud/Wind row (a genuine choice each), not inside it, since
   this is a rolled outcome with a button and a result display rather
   than another set of radio buttons. */
.weather-rain-section { margin: 1rem 0 0.4rem; }
.weather-rain-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1rem;
  margin: 0.3rem 0 0.7rem;
}
.weather-rain-legend-item {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.8rem;
  color: var(--ink);
}
.weather-rain-legend-range { font-size: 0.72rem; color: var(--ink-soft); font-style: italic; }
.weather-rain-result {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.6rem;
  padding: 0.4rem 0.6rem;
  background: rgba(59, 130, 246, 0.08);
  border: 1px solid rgba(59, 130, 246, 0.3);
  border-radius: 6px;
  font-size: 0.85rem;
  color: var(--ink);
}
.weather-rain-result-label { font-weight: 600; }
.weather-rain-none { font-size: 0.8rem; color: var(--ink-soft); font-style: italic; margin-top: 0.6rem; }

/* Party Leader */
.clock-party {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 0.3rem 0.6rem;
  color: var(--parchment);
  font-family: inherit;
  font-size: 0.85rem;
  cursor: default;
  white-space: nowrap;
}
.party-modal { max-width: 340px; }
.party-options { display: flex; flex-direction: column; gap: 0.3rem; margin: 0.8rem 0; }

/* Leader Cards -- the Front/Back buttons sit right after the Party Leader
   label on the toolbar row (see clock-bar-divider/clock-party above). */
.leader-card-buttons { display: flex; gap: 0.4rem; }

/* The base .modal caps at max-width: 520px, too narrow for a 750px image
   box (it would just get squeezed down by .leader-card-image's own
   max-width: 100% safety net below) -- same reasoning as
   .modal-place-view/.modal-character-view's own overrides elsewhere in
   this file, just for this modal instead of an entity's "opened" view. */
.leader-card-modal { max-width: 820px; }

/* 750x750 box, object-fit: contain so Erik's (or anyone's) actual image
   aspect ratio is never stretched/cropped -- just centered and letterboxed
   within the box. A deliberately simple starting point (matching
   .modal-note-image's existing max-width/max-height + object-fit
   technique above, just a bigger fixed box), not yet responsive to each
   image's native size -- revisit if 750x750 turns out too small/large in
   practice. */
.leader-card-image {
  width: 750px;
  height: 750px;
  max-width: 100%;
  border-radius: 8px;
  background: #d9caa0;
  border: 1px solid #b9a061;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  margin: 0.8rem auto;
}
.leader-card-image img { max-width: 100%; max-height: 100%; object-fit: contain; }

/* Party Tension -- same button shell as .clock-party above, just its own
   class so the two can be styled independently later if needed. */
.clock-tension {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 0.3rem 0.6rem;
  color: var(--parchment);
  font-family: inherit;
  font-size: 0.85rem;
  cursor: default;
  white-space: nowrap;
}
.tension-modal { max-width: 460px; }

/* A single horizontal row of 1-10, not PARTY_MEMBERS' vertical name list
   above -- a numeric scale reads better left-to-right. Each option is a
   plain radio input visually rebuilt as a small square "chip": the
   native input is visually hidden (not display:none, so it stays
   keyboard/screen-reader focusable) and :has(:checked) on the wrapping
   label handles the selected-state highlight with no JS class binding
   needed. */
.tension-options { display: flex; gap: 0.35rem; margin: 0.8rem 0; flex-wrap: wrap; }
.tension-option {
  position: relative;
  width: 2.4rem;
  height: 2.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--ink-soft);
  border-radius: 6px;
  font-family: 'Cinzel', serif;
  font-weight: 600;
  font-size: 1rem;
  color: var(--ink);
  cursor: pointer;
  background: rgba(0, 0, 0, 0.03);
}
.tension-option:hover { border-color: var(--gold-dim); }
.tension-option:has(input:checked) {
  background: var(--gold-bright);
  border-color: var(--gold-bright);
  color: #2c2213;
}
.tension-option input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

/* Quest Log -- a fixed 800x800 window, unlike every other modal in this
   app (all auto-height): a two-pane layout (sidebar + scrollable list)
   needs a stable frame to lay out against. Overrides .modal's default
   padding to 0 since the sidebar and main pane each need their own, and
   sets overflow:hidden on the outer shell since the two INNER panes
   handle their own scrolling instead (the sidebar never needs to scroll;
   .quest-list scrolls on its own so the header/Add Quest button above it
   stay put). */
.quest-log-modal {
  width: 1040px; /* 800px * 1.3 -- 30% bigger, per the feature request */
  height: 1040px;
  max-width: 95vw;
  max-height: 90vh;
  padding: 0;
  display: flex;
  overflow: hidden;
}
.quest-log-sidebar {
  width: 170px;
  flex-shrink: 0;
  background: rgba(0, 0, 0, 0.06);
  border-right: 1px solid var(--gold-dim);
  padding: 1rem 0.6rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.quest-flag-btn {
  text-align: left;
  padding: 0.5rem 0.7rem;
  border-radius: 6px;
  border: 1px solid transparent;
  background: transparent;
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  color: var(--ink);
  cursor: pointer;
}
.quest-flag-btn:hover { background: rgba(0, 0, 0, 0.08); }
.quest-flag-active {
  background: var(--gold-bright);
  border-color: var(--gold-bright);
  color: #2c2213;
  font-weight: 600;
}
.quest-log-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  padding: 1.2rem 1.4rem;
  overflow: hidden;
}
.quest-log-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.6rem;
}
.quest-log-header h3 { margin: 0; }
.quest-add-btn { align-self: flex-start; margin-bottom: 0.8rem; }
.quest-list {
  flex: 1;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.quest-row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.5rem;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.03);
  border: 1px solid var(--ink-soft);
}
.quest-thumbnail {
  width: 96px;
  height: 96px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.2rem;
  background: #e3d3a5;
  border: 1px solid var(--gold-dim);
  border-radius: 6px;
  overflow: hidden;
}
.quest-thumbnail img { width: 100%; height: 100%; object-fit: cover; }
.quest-row-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.quest-name-btn {
  text-align: left;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: 'Cinzel', serif;
  font-size: 1.2rem;
  color: var(--ink);
  font-weight: 600;
}
.quest-name-btn:hover { text-decoration: underline; }
.quest-subquest-line { font-size: 0.72rem; font-style: italic; color: var(--ink-soft); }
.quest-objectives-line { font-size: 0.75rem; color: var(--ink-soft); }
.quest-row-actions { display: flex; gap: 2px; flex-shrink: 0; }
.quest-list-empty { color: var(--ink-soft); font-style: italic; }
.quest-detail-note { font-size: 0.8rem; font-style: italic; color: var(--ink-soft); margin-top: 0.6rem; }

/* Quest Modal -- opened by "+ Add Quest" or by clicking a quest's name in
   the Quest Log. Wider than the standard .modal (which caps at 520px) and
   internally split into a fixed header+tab row plus a scrolling body, so
   a quest with a long description/many objectives doesn't scroll the
   tabs out of view along with the content. */
.quest-modal {
  max-width: 955.5px; /* 1365px * 0.7 -- 30% narrower, per the feature
  request (this is the SAME 1365px from the earlier 700->1050->1365px
  history, just now scaled back down) -- narrower so the Player Notes/GM
  Notes text wraps to more, shorter lines and reads better as a block
  rather than a few very long lines. */
  width: 100%;
  max-height: 90vh;
  display: flex;
  overflow: hidden;
}
.quest-modal-inner { display: flex; flex-direction: column; min-height: 0; flex: 1; }
.quest-modal-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.4rem; }
.quest-modal-title-block { flex: 1; min-width: 0; }
.quest-modal-header h3 { margin: 0; }
.quest-modal-title-block .quest-subquest-line { margin: 0.1rem 0 0; }
.quest-status-badge {
  font-family: 'Cinzel', serif;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 8px;
  border-radius: 10px;
  background: var(--gold-dim);
  color: #2c2213;
  white-space: nowrap;
}
.quest-modal-tabs {
  display: flex;
  gap: 0.3rem;
  border-bottom: 1px solid var(--gold-dim);
  margin-bottom: 0.8rem;
  flex-wrap: wrap;
}
.quest-modal-tab {
  padding: 0.4rem 0.7rem;
  border: none;
  border-bottom: 2px solid transparent;
  background: transparent;
  cursor: pointer;
  font-family: 'Cinzel', serif;
  font-size: 0.8rem;
  color: var(--ink-soft);
}
.quest-modal-tab:hover { color: var(--ink); }
.quest-modal-tab-active { color: var(--ink); border-bottom-color: var(--gold-bright); font-weight: 600; }
.quest-modal-body { overflow-y: auto; flex: 1; min-height: 0; padding-right: 0.3rem; }

.quest-modal-top-row { display: flex; gap: 1rem; margin-bottom: 1rem; }
.quest-cover-wrap { flex-shrink: 0; display: flex; flex-direction: column; gap: 0.4rem; align-items: center; }
.quest-cover-image {
  width: 120px;
  height: 120px;
  border-radius: 8px;
  overflow: hidden;
  background: #e3d3a5;
  border: 1px solid var(--gold-dim);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 3rem;
}
.quest-cover-image img { width: 100%; height: 100%; object-fit: cover; }
.quest-cover-clickable { cursor: zoom-in; }
.quest-cover-placeholder { opacity: 0.6; }
.quest-cover-upload { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; }
.quest-fields { flex: 1; display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; }
.quest-readonly-text { margin: 0; font-size: 0.92rem; color: var(--ink); }

.quest-section { margin-bottom: 1.1rem; }
.quest-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
.quest-section-title { margin: 0 0 0.4rem; font-family: 'Cinzel', serif; font-size: 0.95rem; color: var(--ink); }
.quest-section-header .quest-section-title { margin-bottom: 0; }
.quest-description-input {
  width: 100%;
  min-height: 90px;
  resize: vertical;
  font-family: inherit;
  font-size: 0.88rem;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}
.quest-description-text { white-space: pre-wrap; line-height: 1.4; }

/* Player Notes/GM Notes auto-grow, Quest Modal only -- scoped to
   .quest-modal specifically so this does NOT affect the same
   .quest-description-input/.secrets-textarea classes as used by the
   Character/Organization/Place modals (this fine-tuning request was
   specifically about the Quest Modal). autoGrowTextarea (canvas.js) sets
   an explicit inline height to fit the content; max-height here is the
   ceiling that stops it growing forever -- 6x the base 90px min-height,
   per the feature request -- beyond which overflow-y switches on the
   scrollbar. resize is turned off since manual dragging would just
   fight with the automatic sizing on the very next keystroke. */
.quest-modal .quest-description-input,
.quest-modal .secrets-textarea {
  max-height: 540px; /* 90px * 6 */
  overflow-y: auto;
  resize: none;
}

.quest-objective-list, .quest-reward-list { display: flex; flex-direction: column; gap: 0.35rem; }
.quest-objective-row, .quest-reward-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.5rem;
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.06);
  border: 1px solid var(--ink-soft);
}
.quest-objective-status {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  cursor: pointer;
  font-size: 0.85rem;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Was 0.7 -- for anyone without gmMode, EVERY status icon on the whole
   list is disabled (see :disabled="!gmMode" in scenario.html), so this
   value is really "how faded do all the status icons look for a
   player", not a subtle one-off "this particular thing is unavailable"
   cue. 0.7 read as washed-out at that scale; nudged up so the icons stay
   clearly legible while still visually distinct from the clickable GM
   state. */
.quest-objective-status:disabled { cursor: default; opacity: 0.92; }
.quest-objective-text, .quest-reward-name { flex: 1; min-width: 0; font-size: 0.88rem; font-weight: 600; color: var(--ink); word-break: break-word; }
.quest-inline-text-input {
  flex: 1;
  min-width: 0;
  font-family: inherit;
  font-size: 0.88rem;
  padding: 0.25rem 0.4rem;
  border-radius: 5px;
  border: 1px solid var(--gold-dim);
  background: #fbf6e6;
  color: var(--ink);
}
.quest-drag-handle { cursor: grab; }

/* A subquest rendered as its own kind of row at the top of the parent's
   Objectives list -- visually distinct (gold-tinted, no checkbox) from a
   real QuestObjective row, since it isn't one: its "done-ness" is its own
   Quest.status, changed via the same action icons the Quest Log uses. */
.quest-subquest-objective-row { background: rgba(240, 205, 122, 0.12); border-color: var(--gold-dim); }
.quest-subquest-tag {
  flex-shrink: 0;
  font-family: 'Cinzel', serif;
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 1px 6px;
  border-radius: 8px;
  background: var(--gold-dim);
  color: #2c2213;
}

/* Cover image lightbox -- transparent/borderless shell so only the image
   itself (plus its own drop shadow) reads as the "window". */
.quest-lightbox { background: transparent; border: none; box-shadow: none; padding: 0; max-width: 90vw; position: relative; }
.quest-lightbox-image {
  display: block;
  max-width: 90vw;
  max-height: 85vh;
  border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7);
  margin: 0 auto;
}
.quest-lightbox-close {
  position: absolute;
  top: -2.2rem;
  right: 0;
  color: var(--parchment);
  font-size: 2rem;
}

/* Presentation Gallery -- saved images that are shown in a shared,
   player-wide overlay rather than placed as canvas cards. */
.presentation-gallery-modal {
  width: min(900px, 94vw);
  max-width: 900px;
}
.presentation-dropzone {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.55rem 0.8rem;
  margin: 0.7rem 0 1rem;
  padding: 1.15rem;
  border: 2px dashed var(--gold-dim);
  border-radius: 8px;
  background: rgba(201, 162, 75, 0.08);
  color: var(--ink-soft);
  text-align: center;
}
.presentation-dropzone strong,
.presentation-dropzone span {
  flex-basis: 100%;
}
.presentation-dropzone strong { color: var(--ink); }
.presentation-dropzone > .btn { flex-basis: auto; }
.presentation-dropzone .upload-status {
  flex-basis: auto;
  font-style: italic;
}
.presentation-gallery-empty {
  margin: 1.4rem 0 0.4rem;
  color: var(--ink-soft);
  text-align: center;
  font-style: italic;
}
.presentation-gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 0.85rem;
  max-height: 58vh;
  overflow-y: auto;
  padding: 0.2rem;
}
.presentation-gallery-item {
  min-width: 0;
  padding: 0.45rem;
  border: 1px solid #b9a061;
  border-radius: 7px;
  background: rgba(255, 255, 255, 0.3);
}
.presentation-gallery-item-active {
  border-color: var(--gold);
  box-shadow: 0 0 0 2px rgba(201, 162, 75, 0.35);
}
.presentation-gallery-preview {
  display: block;
  width: 100%;
  height: 130px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  overflow: hidden;
  background: #2a2015;
  cursor: pointer;
}
.presentation-gallery-preview img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.presentation-gallery-item-footer {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0.4rem 0;
  min-width: 0;
}
.presentation-gallery-active {
  flex-shrink: 0;
  color: var(--ok);
  font-family: 'Cinzel', serif;
  font-size: 0.62rem;
  text-transform: uppercase;
}
.presentation-gallery-item-actions {
  display: flex;
  gap: 0.35rem;
}
.presentation-gallery-item-actions .btn {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Shared presentation -- no click-outside or Escape dismissal is provided:
   the GM's explicit control is the only way to hide it for the table. */
.presentation-overlay {
  position: fixed;
  inset: 0;
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(0.8rem, 3vw, 2rem);
  background: rgba(0, 0, 0, 0.88);
}
.presentation-stage {
  display: flex;
  flex-direction: column;
  width: min(1200px, 96vw);
  height: min(94vh, 900px);
  gap: 0.65rem;
}
.presentation-header {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 1rem;
  min-height: 2.1rem;
  color: var(--parchment);
}
.presentation-actions {
  display: flex;
  flex-shrink: 0;
  gap: 0.45rem;
}
.presentation-image {
  display: block;
  width: 100%;
  height: 100%;
  min-height: 0;
  object-fit: contain;
  border-radius: 6px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7);
}

/* Dice Roller */
.dice-roller-modal { max-width: 624px; }
/* Turns the Dice Roller into a floating, non-blocking panel instead of a
   centered, backdrop-blocking dialog -- see the feature request's own
   comment in scenario.html for the full reasoning. Bottom-right corner:
   out of the way of the main toolbar (top) and clear of most canvas
   content (which tends to grow from the top-left), while still landing
   somewhere predictable rather than wherever the last click happened to
   be. The .modal base class already provides the background/border/
   shadow/padding -- this only adds the positioning a backdrop-centered
   modal would otherwise have handled. */
.dice-roller-floating {
  position: fixed;
  bottom: 20px;
  right: 20px;
  left: auto;
  top: auto;
  margin: 0;
  z-index: 100; /* same layer .modal-backdrop used to provide */
  width: auto;
  max-width: min(624px, calc(100vw - 40px));
  max-height: calc(100vh - 40px);
  overflow-y: auto;
}
.dice-type-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.8rem 0;
}
.dice-type-btn {
  flex: 1 1 auto;
  min-width: 3.2rem;
  padding: 0.45rem 0.3rem;
  border-radius: 8px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.dice-type-btn:hover:not(:disabled) { border-color: var(--gold-bright); transform: translateY(-1px); }
.dice-type-btn:disabled { opacity: 0.6; cursor: default; }
.dice-type-selected {
  background: var(--gold-bright);
  border-color: var(--gold-bright);
  color: #2c2213;
}

.dice-controls-row { display: flex; gap: 0.8rem; margin-bottom: 0.9rem; }
.dice-control-label {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-family: 'Cinzel', serif;
  font-size: 0.78rem;
  color: var(--ink-soft);
}
.dice-control-label input {
  font-family: inherit;
  font-size: 0.9rem;
  padding: 0.4rem 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}

.dice-roll-btn { width: 100%; font-size: 1rem; padding: 0.6rem; margin-bottom: 1rem; }
.dice-roll-btn:disabled { opacity: 0.7; cursor: default; }

/* Fixed min-height so the modal doesn't jump between the three result
   states (obscured/revealed/empty). */
.dice-result-area {
  min-height: 108px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.04);
  border: 1px solid var(--ink-soft);
  margin-bottom: 1rem;
  overflow: hidden;
}

.dice-obscure { display: flex; align-items: center; justify-content: center; gap: 1.4rem; }
/* A soft glowing "orb" behind each rune, on its own gentle pulse -- this
   is also what fixes the "clunky white background" look: each rune GIF
   has an opaque white square background (not transparent), which
   mix-blend-mode: multiply (below, on .dice-rune itself) blends away
   against whatever's behind it -- here, this warm gold glow -- instead
   of sitting on top of it as a stark white box. */
.dice-rune-wrap {
  position: relative;
  width: 84px;
  height: 84px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(240, 205, 122, 0.55) 0%, rgba(240, 205, 122, 0.18) 55%, transparent 78%);
  animation: dice-orb-glow 1.8s ease-in-out infinite;
}
.dice-rune-wrap-fate { animation-delay: 0.4s; }
.dice-rune {
  width: 58px;
  height: 58px;
  mix-blend-mode: multiply;
  animation: dice-rune-pulse 0.9s ease-in-out infinite, dice-rune-flash 1.1s ease-in-out infinite;
}
/* Fate rune pulses slightly out of phase with Luck -- two runes flashing
   in perfect unison reads as one blinking icon; staggered, it reads as
   two independent things happening, which is the "fancy" the feature
   asked for. */
.dice-rune-wrap-fate .dice-rune { animation-delay: 0.35s, 0.5s; }

.dice-result-reveal {
  text-align: center;
  animation: dice-result-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.dice-result-hidden-badge {
  display: block;
  font-family: 'Cinzel', serif;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--danger);
  margin-bottom: 0.3rem;
}
.dice-result-total {
  font-family: 'Cinzel', serif;
  font-size: 2.8rem;
  font-weight: 700;
  /* Darker, high-contrast fill (was a light gold, hard to read against
     the parchment background) -- the gold now lives only in the glow
     around the text, not the fill itself. */
  color: #3a1f0d;
  text-shadow: 0 0 10px rgba(240, 205, 122, 0.6);
  line-height: 1;
}
.dice-result-breakdown { font-size: 0.82rem; color: var(--ink-soft); margin-top: 0.35rem; }
.dice-result-empty { color: var(--ink-soft); font-style: italic; font-size: 0.85rem; }

.dice-history { max-height: 160px; overflow-y: auto; }
.dice-history-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem; }
.dice-history-title {
  margin: 0;
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  color: var(--ink);
}
.dice-history-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.4rem;
  font-size: 0.8rem;
  border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.dice-history-roller {
  flex-shrink: 0;
  font-family: 'Cinzel', serif;
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  min-width: 2.6rem;
  max-width: 5.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dice-history-detail { flex: 1; min-width: 0; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dice-history-hidden-icon { flex-shrink: 0; font-size: 0.75rem; }
.dice-history-total { flex-shrink: 0; font-weight: 700; color: var(--ink); }

.dice-hide-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.82rem;
  color: var(--ink-soft);
  margin-bottom: 0.9rem;
  cursor: pointer;
}
.dice-hide-note { font-size: 0.78rem; color: var(--ink-soft); font-style: italic; margin: 0 0 0.9rem; }

/* Rolemaster fumble/open-ended prompt (see handle_dice_roll's comment,
   app.py) -- takes over the same result-area slot the runes/final number
   otherwise occupy. */
.dice-prompt { text-align: center; padding: 0 0.5rem; }
.dice-prompt-message {
  margin: 0 0 0.3rem;
  font-family: 'Cinzel', serif;
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
}
.dice-prompt-rolls { margin: 0 0 0.7rem; font-size: 0.8rem; color: var(--ink-soft); }
.dice-prompt-actions { display: flex; gap: 0.6rem; justify-content: center; }
.dice-special-badge {
  display: block;
  font-family: 'Cinzel', serif;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--gold-bright);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
  margin-bottom: 0.3rem;
}

@keyframes dice-orb-glow {
  0%, 100% { transform: scale(1); opacity: 0.75; }
  50% { transform: scale(1.12); opacity: 1; }
}
@keyframes dice-rune-pulse {
  0%, 100% { transform: scale(1) rotate(0deg); filter: drop-shadow(0 0 4px #f0cd7a); }
  50% { transform: scale(1.18) rotate(6deg); filter: drop-shadow(0 0 18px #f0cd7a); }
}
@keyframes dice-rune-flash {
  0%, 40% { opacity: 1; }
  50% { opacity: 0.3; }
  60%, 100% { opacity: 1; }
}
@keyframes dice-result-pop {
  0% { opacity: 0; transform: scale(0.3) rotate(-12deg); }
  55% { opacity: 1; transform: scale(1.2) rotate(5deg); }
  75% { transform: scale(0.92) rotate(-2deg); }
  100% { opacity: 1; transform: scale(1) rotate(0deg); }
}

/* Set Date / Set Time modals */
.clock-date-fields, .clock-time-fields { display: flex; gap: 0.8rem; margin: 0.6rem 0; }
.clock-field-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-family: 'Cinzel', serif;
  font-size: 0.8rem;
  color: var(--ink-soft);
  flex: 1;
}
.clock-field-label input, .clock-field-label select {
  font-family: inherit;
  font-size: 0.95rem;
  padding: 0.4rem 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}
.clock-date-preview { color: var(--ink-soft); font-size: 0.9rem; margin: 0.4rem 0 0.8rem; }
.clock-date-preview strong { color: var(--ink); }

.clock-nudge-heading, .clock-rate-heading {
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin: 0.8rem 0 0.4rem;
}
.clock-rate-hint { font-weight: 400; font-style: italic; opacity: 0.8; }
.clock-nudge-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.clock-nudge-note { font-size: 0.78rem; color: var(--ink-soft); font-style: italic; margin: 0.5rem 0 0; }
.clock-time-presets {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.45rem;
  margin-bottom: 0.8rem;
}
.clock-time-preset {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-width: 0;
}
.clock-time-preset-value {
  font-variant-numeric: tabular-nums;
  opacity: 0.72;
}
@media (max-width: 560px) {
  .clock-time-presets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Set Date modal: widened 100% (520px -> 1040px) to make room for the
   Calendar Notes section below the date fields/quick-adjust row. Scoped
   to this one modal specifically, not the shared .modal base class, so
   every other modal keeps its normal size. */
.clock-date-modal { max-width: 1040px; }
.clock-notes-label { display: block; margin-top: 1rem; }
.clock-notes-textarea {
  width: 100%;
  min-height: 120px;
  resize: vertical;
  font-family: inherit;
  font-size: 0.9rem;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}
.clock-notes-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 0.3rem;
}
.clock-notes-counter { font-size: 0.78rem; color: var(--ink-soft); margin: 0; }
.clock-modal-divider { border: none; border-top: 1px solid var(--ink-soft); opacity: 0.3; margin: 1rem 0; }
.clock-rate-options { display: flex; flex-direction: column; gap: 0.4rem; }
.clock-rate-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: 'Cinzel', serif;
  font-size: 0.9rem;
  color: var(--ink);
  cursor: pointer;
}

.scenario-title {
  font-family: 'Cinzel', serif;
  font-size: 1.2rem;
  margin: 0;
  color: var(--gold-bright);
}

.live-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--ok);
  box-shadow: 0 0 8px var(--ok);
}
.live-dot-off { background: var(--danger-bright); box-shadow: 0 0 8px var(--danger-bright); }

.gm-toggle {
  display: flex; align-items: center; gap: 0.45rem;
  font-family: 'Cinzel', serif; font-size: 0.85rem; color: var(--parchment-dark);
  cursor: pointer; user-select: none;
}

/* Login page -- the whole app's front door now (see _require_login,
   app.py). No Alpine here at all, deliberately: it's a plain HTML form
   POST, which works even with JS disabled or slow to load, exactly what
   you want from the one page that has to work before anything else can. */
.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.login-card {
  width: 100%;
  max-width: 400px;
  background: linear-gradient(180deg, var(--parchment), var(--parchment-dark));
  border: 1px solid #b9a061;
  border-radius: 14px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
  padding: 2rem 1.8rem;
  text-align: center;
}
.login-title {
  font-family: 'Cinzel Decorative', 'Cinzel', serif;
  font-weight: 900;
  font-size: 1.7rem;
  margin: 0 0 0.2rem;
  color: var(--ink);
}
.login-subtitle { margin: 0 0 1.2rem; color: var(--ink-soft); font-size: 0.85rem; }
.login-error {
  background: rgba(154, 51, 36, 0.12);
  border: 1px solid var(--danger);
  color: var(--danger);
  border-radius: 6px;
  padding: 0.5rem 0.7rem;
  font-size: 0.85rem;
  margin: 0 0 1rem;
}
.login-form { display: flex; flex-direction: column; gap: 0.7rem; text-align: left; }
.login-section-title {
  margin: 0;
  font-family: 'Cinzel', serif;
  font-size: 1rem;
  color: var(--ink);
  text-align: center;
}
.login-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-family: 'Cinzel', serif;
  font-size: 0.78rem;
  color: var(--ink-soft);
}
.login-field input, .login-field select {
  font-family: inherit;
  font-size: 0.95rem;
  padding: 0.5rem 0.6rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}
.login-submit { width: 100%; margin-top: 0.2rem; }
.login-divider {
  margin: 1.3rem 0;
  text-align: center;
  font-size: 0.75rem;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  position: relative;
}
.login-divider::before, .login-divider::after {
  content: "";
  position: absolute;
  top: 50%;
  width: 40%;
  height: 1px;
  background: var(--ink-soft);
  opacity: 0.3;
}
.login-divider::before { left: 0; }
.login-divider::after { right: 0; }
.login-note { color: var(--ink-soft); font-style: italic; font-size: 0.85rem; margin: 0; }

.canvas-viewport {
  flex: 1;
  overflow: auto;
  background:
    linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px),
    radial-gradient(ellipse at center, #241c12 0%, #140f0a 80%);
  background-size: 40px 40px, 40px 40px, cover;
}
.canvas-viewport.route-draw-cursor { cursor: crosshair; }

.canvas-content { position: relative; }

.route-layer { position: absolute; top: 0; left: 0; pointer-events: none; }
.route-group { pointer-events: auto; cursor: pointer; }
.route-hit { stroke: transparent; stroke-width: 16; fill: none; }
/* Bumped from 4/5px -- Janne asked for better visibility across the board. */
.route-line { stroke: var(--gold); stroke-width: 5; fill: none; }
.route-line-selected { stroke: var(--danger-bright); stroke-width: 6; filter: drop-shadow(0 0 6px rgba(201, 75, 54, 0.8)); }
.route-label {
  font-family: 'Cinzel', serif;
  font-size: 13px;
  fill: var(--gold-bright);
  paint-order: stroke fill;
  stroke: rgba(10, 8, 4, 0.9);
  stroke-width: 3px;
  stroke-linejoin: round;
  pointer-events: none; /* the wide, invisible .route-hit path is the click target */
  user-select: none;
}
.route-label-selected { fill: #ffe4b0; }
/* The sub-type name (Ally, Trade, Kinship / Family, ...) drawn along a
   Diplomacy/Leverage/Affinity relation. Its fill color is set inline per
   route (matches the line color) so it wins over both .route-label's and
   .route-label-selected's fill -- only weight/tracking differ here. */
.route-type-label { font-weight: 700; letter-spacing: 0.03em; }
.arrowhead-path { fill: var(--gold); }
.arrowhead-path-selected { fill: var(--danger-bright); }
.route-preview { stroke: var(--gold-bright); stroke-width: 3; stroke-dasharray: 8 6; fill: none; opacity: 0.85; }
.scale-preview { stroke: var(--parchment); stroke-width: 2; stroke-dasharray: 4 4; fill: none; opacity: 0.9; }

/* Measure Distance -- the path itself is drawn in the same SVG layer as
   routes (see measureMarkup() in canvas.js); the two floating badges below
   are plain HTML, positioned in raw viewport coordinates so they track the
   mouse (tooltip) or sit fixed in a screen corner (result) regardless of
   canvas scroll. */
.measure-line { stroke: var(--gold-bright); stroke-width: 2.5; stroke-dasharray: 6 4; fill: none; }
.measure-point { fill: var(--gold-bright); stroke: var(--ink); stroke-width: 1; }
.measure-segment-label {
  font-family: 'Cinzel', serif;
  font-size: 12px;
  fill: var(--parchment);
  paint-order: stroke fill;
  stroke: rgba(10, 8, 4, 0.9);
  stroke-width: 3px;
  stroke-linejoin: round;
  pointer-events: none;
}

.measure-tooltip {
  position: fixed;
  z-index: 60;
  pointer-events: none; /* never intercept the click meant for the canvas beneath it */
  background: rgba(20, 15, 8, 0.92);
  color: var(--gold-bright);
  border: 1px solid var(--gold-dim);
  border-radius: 6px;
  padding: 0.3rem 0.55rem;
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  white-space: nowrap;
  box-shadow: 0 3px 10px var(--shadow);
}
.measure-tooltip-hint {
  display: block;
  font-family: inherit;
  font-size: 0.65rem;
  font-weight: 400;
  color: var(--parchment-dark);
  margin-top: 0.1rem;
}

.measure-result {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.7rem;
  background: linear-gradient(180deg, var(--parchment), var(--parchment-dark));
  border: 1px solid #b9a061;
  border-radius: 8px;
  box-shadow: 0 4px 14px var(--shadow);
  font-family: 'Cinzel', serif;
  color: var(--ink);
}
.measure-result-clear {
  background: transparent;
  border: none;
  color: var(--ink-soft);
  cursor: pointer;
  font-size: 0.9rem;
  padding: 0 0.2rem;
  line-height: 1;
}
.measure-result-clear:hover { color: var(--danger); }
.route-line-hidden { stroke-dasharray: 3 7; opacity: 0.55; }
.route-label-hidden { opacity: 0.55; }

/* ---------- "Add Relation" try-out: category/subtype styling ---------- */
/* Color/width/dasharray for a styled relation are set inline per-route (see
   relationMarkup() in canvas.js, since they vary per subtype) -- these
   classes only add effects that don't vary by instance: glow and the
   flowing-dash animation. Selecting a styled relation keeps its category
   color (the inline style wins over .route-line-selected's color/width)
   but still gets .route-line-selected's drop-shadow glow, so selection
   stays visible without hiding what category the relation is. */
.relation-glow-ally { filter: drop-shadow(0 0 5px rgba(37, 99, 235, 0.85)); }
@keyframes relation-pulse-glow {
  0%, 100% { filter: drop-shadow(0 0 2px rgba(225, 29, 72, 0.35)); }
  50% { filter: drop-shadow(0 0 7px rgba(225, 29, 72, 0.9)); }
}
.relation-glow-romance { animation: relation-pulse-glow 2.2s ease-in-out infinite; }
@keyframes relation-flow-dash {
  to { stroke-dashoffset: -20; }
}
.relation-anim-flow { animation: relation-flow-dash 1.2s linear infinite; }

/* Add Relation picker modal (category grid, then subtype grid) */
.relation-picker-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
  margin: 0.4rem 0 0.2rem;
}
.relation-picker-option {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.7rem 0.5rem;
  text-align: center;
}
.relation-picker-swatch {
  display: inline-block;
  width: 0.8rem;
  height: 0.8rem;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.35);
  flex-shrink: 0;
}

/* Hide-until-revealed: a GM-only "ghost preview" look shared by every
   entity card/frame so a hidden item still reads clearly as hidden no
   matter which card type it's wrapping. Players never see this class --
   hidden items are removed from their view entirely (x-show="!hidden"),
   so this is purely a GM-mode prep aid. */
.gm-hidden-preview {
  opacity: 0.55;
  outline: 2px dashed var(--danger-bright);
  outline-offset: 3px;
}
.hidden-badge {
  display: inline-block;
  margin: 4px 0 0 4px;
  padding: 1px 7px;
  font-family: 'Cinzel', serif;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--parchment);
  background: var(--danger);
  border: 1px solid var(--danger-bright);
  border-radius: 4px;
  box-shadow: 0 2px 6px var(--shadow);
  pointer-events: none;
  user-select: none;
  position: relative;
  z-index: 5;
}

/* Relation snapping: while a relation is being drawn, whichever entity is
   already locked in as the start point (relation-anchor-start) or currently
   under the cursor as a candidate end point (relation-anchor-hover) gets
   outlined, so the GM can see exactly what a release will snap to. Applied
   to Frame/Note/Character/Organization/Place/MapIcon alike -- same class names,
   shared look, regardless of which entity type it's wrapping. */
@keyframes relation-anchor-pulse {
  0%, 100% { outline-color: var(--gold-bright); }
  50% { outline-color: var(--gold-dim); }
}
.relation-anchor-start {
  outline: 3px solid var(--gold-bright);
  outline-offset: 3px;
}
.relation-anchor-hover {
  outline: 3px dashed var(--gold-bright);
  outline-offset: 5px;
  animation: relation-anchor-pulse 0.9s ease-in-out infinite;
}

.note-box {
  position: absolute;
  background: linear-gradient(180deg, var(--parchment), var(--parchment-dark));
  border: 2px solid #a88c4f;
  border-radius: 8px;
  box-shadow: 0 6px 16px var(--shadow);
  color: var(--ink);
  cursor: grab;
  overflow: hidden;
  touch-action: none;
  user-select: none;
}
.note-box.note-editing { border-color: var(--gold-bright); box-shadow: 0 0 0 3px rgba(240, 205, 122, 0.4), 0 6px 16px var(--shadow); cursor: default; }

.note-icons {
  display: flex;
  gap: 2px;
  background: rgba(20, 15, 8, 0.78);
  padding: 3px 4px;
}
.note-icon {
  background: transparent;
  border: none;
  font-size: 0.85rem;
  padding: 2px 5px;
  cursor: pointer;
  border-radius: 4px;
  line-height: 1.2;
}
.note-icon:hover { background: rgba(255,255,255,0.15); }
.note-icon-disabled { opacity: 0.35; cursor: default; }
.note-icon-disabled:hover { background: transparent; }

.note-display { padding: 0.5rem 0.6rem; height: calc(100% - 26px); display: flex; flex-direction: column; position: relative; }
.note-text-mode { display: flex; flex-direction: column; height: 100%; }
.note-label { font-family: 'Cinzel', serif; font-size: 0.9rem; margin: 0 0 0.35rem; color: var(--ink); }
.note-text-preview {
  font-size: 0.82rem;
  line-height: 1.3;
  margin: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 6;
  -webkit-box-orient: vertical;
}
/* Still used by Place's card (place.image_path) -- Note's own cover-image
   mode below no longer uses this, but Place's simpler "small thumb below
   the label" layout still does, so this stays. */
.note-thumb {
  flex: 1;
  border-radius: 4px;
  background-size: cover;
  background-position: center;
  border: 1px solid #b9a061;
}
/* Cover-image mode -- fills the FULL card face edge to edge (cancels
   .note-display's own padding via position:absolute/inset:0, since
   .character-portrait below expects to be a direct, unpadded child the
   way it is on .character-card -- .note-display's padding exists for
   the plain-text layout above and shouldn't shrink the cover image). */
.note-cover-mode { position: absolute; inset: 0; }

/* ---------- Character cards ---------- */

.character-card {
  position: absolute;
  width: 200px;
  height: 200px;
  border-radius: 10px;
  border: 3px solid var(--gold-dim);
  box-shadow: 0 6px 16px var(--shadow);
  cursor: grab;
  overflow: hidden;
  touch-action: none;
  user-select: none;
  background: linear-gradient(180deg, #3a2f1e, #1c160d);
}

.character-icons {
  position: absolute;
  top: 0;
  right: 0;
  display: flex;
  gap: 2px;
  background: rgba(20, 15, 8, 0.78);
  padding: 3px 4px;
  border-bottom-left-radius: 6px;
  z-index: 2;
}

.character-portrait {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center top;
}
.character-portrait-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 3.5rem;
  color: var(--gold-dim);
  background: radial-gradient(ellipse at center, #3a2f1e, #1c160d);
}

.character-plate {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0.35rem 0.55rem 0.45rem;
  background: linear-gradient(0deg, rgba(10, 8, 4, 0.92), rgba(10, 8, 4, 0.05));
  color: var(--parchment);
}
.character-name {
  font-family: 'Cinzel', serif;
  font-size: 0.9rem;
  font-weight: 600;
  text-shadow: 0 1px 2px #000;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.character-faction {
  font-size: 0.72rem;
  font-style: italic;
  color: var(--gold-bright);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.modal-character { max-width: 460px; }
.character-name-input {
  font-family: 'Cinzel', serif;
  font-size: 1.1rem;
  padding: 0.3rem 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
  flex: 1;
}
.character-details { margin: 0.4rem 0; }
.character-detail-line { margin: 0.2rem 0; font-size: 0.92rem; }
.character-edit-form { display: flex; flex-direction: column; gap: 0.7rem; margin: 0.6rem 0; }

/* Character personality/social profile -- 8 dropdowns is a lot to add to
   an already-busy modal, so (same "tightly packed" spirit as the
   Organization clan stats below) these render as a compact 2-column grid
   of small selects rather than 8 full-width character-field-label rows,
   with a matching compact view-mode summary that only shows fields the
   GM has actually set (nothing to show for a character nobody's
   evaluated yet). Strengths/Weaknesses pair up in their own 2-column row
   since they're natural opposites. */
.character-trait-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem 0.6rem;
}
.character-trait-field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-family: 'Cinzel', serif;
  font-size: 0.75rem;
  color: var(--ink-soft);
}
.character-trait-field select {
  font-family: inherit;
  font-size: 0.82rem;
  padding: 0.3rem 0.35rem;
  border-radius: 5px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
  width: 100%;
}
.character-trait-view {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.1rem 0.6rem;
  margin: 0.3rem 0;
}
.character-trait-view-item { font-size: 0.85rem; color: var(--ink); }
.character-pair-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
}

/* Organization "Is a Clan" resource stats -- deliberately compact (per the
   feature request) so a 6-field extra doesn't bloat the modal: a tight
   3-column grid rather than 6 full-width character-field-label rows, and
   a small one-line-per-item summary in view mode rather than 6 separate
   <p class="character-detail-line"> blocks. */
.clan-checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  color: var(--ink-soft);
  cursor: pointer;
}
.clan-stats-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.4rem 0.5rem;
}
.clan-stat-field {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-family: 'Cinzel', serif;
  font-size: 0.7rem;
  color: var(--ink-soft);
}
.clan-stat-field input {
  font-family: inherit;
  font-size: 0.85rem;
  padding: 0.25rem 0.3rem;
  border-radius: 5px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
  width: 100%;
}
/* Warfare's two-input "pair" cell (Attack/Defense) -- same footprint as
   every other clan-stat-field cell, just split into two narrower inputs
   side by side instead of one full-width number input. */
.clan-stat-pair { display: flex; gap: 0.25rem; }
.clan-stat-pair input { min-width: 0; }
.clan-stats-view {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.1rem 0.6rem;
  margin: 0.3rem 0;
}
.clan-stat-view-item { font-size: 0.8rem; color: var(--ink); white-space: nowrap; }

/* ---------- Bigger "opened" detail modals ----------
   Note/Character/Organization/Place are the four canvas cards you click to
   open; their detail modals get a deliberate bump over the shared
   .modal/.modal-note/.modal-character base sizing so the OPENED view reads
   as bigger -- the on-canvas token itself stays its normal size (see
   NOTE_SIZE/CHARACTER_SIZE/ORGANIZATION_SIZE/PLACE_SIZE in canvas.js).
   Note matches Character/Organization's 690px exactly (not Place's 780px);
   everything else about it (padding, image height, text size) was already
   shared, so width was the only real difference between them. Scoped
   through these four modifier classes (added alongside the base classes in
   scenario.html) rather than touching .modal-note/.modal-character
   directly, since those base classes are also shared with modals that must
   stay their current size: the Relation/Add Icon pickers and the background-image
   modal all use .modal-note, and the Route detail modal uses
   .modal-character. */
.modal-place-view { max-width: 780px; }
.modal-note-view, .modal-character-view, .modal-organization-view { max-width: 690px; }
.modal-note-view, .modal-place-view, .modal-character-view, .modal-organization-view {
  padding: 2.4rem;
}
.modal-note-view h3, .modal-place-view h3,
.modal-character-view h3, .modal-organization-view h3 {
  font-size: 1.75rem;
}
.modal-note-view .modal-note-image, .modal-place-view .modal-note-image,
.modal-character-view .modal-note-image, .modal-organization-view .modal-note-image {
  height: 480px;
}
.modal-note-view .modal-note-text, .modal-place-view .modal-note-text,
.modal-character-view .modal-note-text, .modal-organization-view .modal-note-text {
  font-size: 1.5rem;
}
.modal-character-view .character-name-input, .modal-organization-view .character-name-input {
  font-size: 1.65rem;
  padding: 0.45rem 0.75rem;
}
.modal-character-view .character-detail-line, .modal-organization-view .character-detail-line {
  font-size: 1.38rem;
}
.modal-character-view .character-field-label, .modal-organization-view .character-field-label {
  font-size: 1.2rem;
}
.modal-character-view .character-field-label input,
.modal-character-view .character-field-label textarea,
.modal-organization-view .character-field-label input,
.modal-organization-view .character-field-label textarea {
  font-size: 1.35rem;
  padding: 0.6rem 0.75rem;
}

.wiki-link {
  display: inline-flex;
  align-items: center;
  margin-top: 1rem;
  padding: 0.38rem 0.78rem;
  border: 1px solid var(--gold-dim);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(240, 205, 122, 0.3), rgba(139, 90, 43, 0.14));
  color: var(--ink);
  font-family: 'Cinzel', serif;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-decoration: underline;
  text-underline-offset: 0.18em;
  box-shadow: 0 2px 8px rgba(47, 25, 10, 0.2);
}
.wiki-link::after {
  content: "↗";
  margin-left: 0.4rem;
  font-family: sans-serif;
  font-size: 0.85em;
}
.wiki-link:hover,
.wiki-link:focus-visible {
  background: linear-gradient(180deg, rgba(240, 205, 122, 0.55), rgba(139, 90, 43, 0.24));
  color: var(--danger);
}

/* ---------- Character runes ---------- */

/* On-canvas display, under the name/faction -- sized to match .note-icon
   (0.85rem/~14px glyph) so a character's runes read as part of the same
   icon-weight UI language as the Edit/Save/Delete buttons, not as a bigger
   decorative element. */
.character-runes { display: flex; gap: 3px; margin-top: 0.2rem; }
.rune-icon {
  width: 16px;
  height: 16px;
  object-fit: contain;
  vertical-align: middle;
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.6));
}
.rune-row { display: inline-flex; gap: 4px; vertical-align: middle; }
.rune-field-hint { font-weight: 400; opacity: 0.75; text-transform: none; }

/* Edit-mode picker -- swatches are a bit bigger than .rune-icon since these
   are tap targets, not just a status display. */
.rune-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  max-height: 140px;
  overflow-y: auto;
  padding: 0.35rem;
  border: 1px solid var(--ink-soft);
  border-radius: 6px;
  background: #fbf6e6;
}
.rune-picker-item {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 2px;
  cursor: pointer;
  line-height: 0;
}
.rune-picker-item img { width: 22px; height: 22px; object-fit: contain; }
.rune-picker-item:hover { background: rgba(0, 0, 0, 0.08); }
.rune-picker-selected { border-color: var(--gold-dim); background: rgba(240, 205, 122, 0.35); }
.rune-picker-disabled { opacity: 0.35; cursor: default; }
.rune-picker-disabled:hover { background: transparent; }

/* Add Icon's grouped picker (Heorthling Clan Village / Settlements). */
.icon-picker-hint { font-size: 0.85rem; color: var(--ink-soft); margin: 0.3rem 0 0; }
.icon-picker-grid { grid-template-columns: repeat(3, 1fr); max-height: 320px; overflow-y: auto; }
.icon-picker-thumb { width: 22px; height: 22px; object-fit: contain; }
.icon-picker-section + .icon-picker-section { margin-top: 1rem; }
.icon-picker-group-title {
  font-family: 'Cinzel', serif;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin: 0.6rem 0 0.4rem;
}
.icon-picker-glyph svg { display: block; width: 100%; height: 100%; }

/* The "Scenario" icon's link picker -- a flat, scrollable list of
   scenario names (usually a short list), each one a full-width button
   that links immediately on click. */
.scenario-link-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  max-height: 320px;
  overflow-y: auto;
  margin-top: 0.6rem;
}
.scenario-link-option { text-align: left; }
.scenario-link-empty { font-size: 0.85rem; color: var(--ink-soft); font-style: italic; }

/* Import dialog */
.modal-import { max-width: 420px; }
.import-loading-hint, .import-empty-hint {
  font-size: 0.85rem;
  color: var(--ink-soft);
  font-style: italic;
  margin: 0.6rem 0 0;
}
.import-card-list {
  margin-top: 0.8rem;
  max-height: 320px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}
.import-card-group { display: flex; flex-direction: column; gap: 0.3rem; }
.import-group-title {
  margin: 0;
  font-family: 'Cinzel', serif;
  font-size: 0.82rem;
  color: var(--ink);
  border-bottom: 1px solid var(--ink-soft);
  padding-bottom: 0.2rem;
}
.import-card-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.88rem;
  color: var(--ink);
  cursor: pointer;
  padding: 0.15rem 0;
}
.import-result-message {
  text-align: center;
  font-family: 'Cinzel', serif;
  font-size: 0.9rem;
  color: var(--gold-bright);
  margin: 1.2rem 0;
}

/* Add Stamp's picker -- a single wide column of banner-shaped previews
   rather than the small-square grid MapIcon's picker uses, since a stamp
   thumbnail is inherently wide (200x100, same aspect as STAMP_DISPLAY_W/H
   in canvas.js) and would look cramped in a multi-column grid. */
.stamp-picker-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-height: 420px;
  overflow-y: auto;
}
.stamp-picker-option {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem;
}
.stamp-picker-thumb {
  display: block;
  width: 160px;
  height: 80px;
}
.stamp-picker-thumb svg { display: block; width: 100%; height: 100%; }

/* A MapIcon marker is deliberately tiny and icon-only (no card, no name
   plate) for most types -- see the MapIcon model docstring for why.
   z-index keeps it reading on top of notes/characters/frames/the
   background, since its whole purpose is to stay visible as a map marker.
   width/height are NOT fixed here -- they're set inline per-instance from
   mapIconDisplaySize() (canvas.js), since the "scenario" icon type
   renders at double size (SCENARIO_MAP_ICON_SIZE) while every other type
   uses the normal MAP_ICON_SIZE. */
.map-icon-marker {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  touch-action: none;
  user-select: none;
  z-index: 3;
}
.map-icon-glyph {
  display: block;
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.75));
}
.map-icon-glyph svg { display: block; width: 100%; height: 100%; }

/* "Focus" icon animations -- see its MAP_ICON_CATALOG entry's comment,
   canvas.js, for why these are plain class-based CSS animations rather
   than SVG <animateTransform>/gradients: every placed Focus icon shares
   this exact same static SVG string, so nothing here can use a unique
   id without colliding the moment two are on the map at once. A CSS
   class has no such problem -- every element using it just runs its own
   independent copy of the same animation. transform-origin here refers
   to the SVG's own 64x64 user-space coordinates (the standard behavior
   for CSS transforms on SVG elements), not CSS pixels, so 32px 32px is
   this icon's exact center regardless of how large it's actually
   rendered on the canvas. */
.focus-ring-outer { animation: focus-ring-spin 9s linear infinite; transform-origin: 32px 32px; }
.focus-ring-inner { animation: focus-ring-spin-reverse 6s linear infinite; transform-origin: 32px 32px; }
.focus-glow-pulse { animation: focus-glow-breathe 2.2s ease-in-out infinite; }
.focus-core-pulse { animation: focus-core-breathe 2.2s ease-in-out infinite; transform-origin: 32px 32px; }
/* Applied to the whole icon's root <svg> -- lets whatever's on the map
   underneath (the entire point of a marker meant to point AT something)
   show through periodically, rather than the icon permanently covering
   it. Mostly opaque so it still reads clearly as a marker, dipping low
   only briefly near the end of each cycle. */
.focus-opacity-shift { animation: focus-opacity-shift 5s ease-in-out infinite; }

@keyframes focus-ring-spin { to { transform: rotate(360deg); } }
@keyframes focus-ring-spin-reverse { to { transform: rotate(-360deg); } }
@keyframes focus-glow-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
@keyframes focus-core-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.18); } }
@keyframes focus-opacity-shift { 0%, 65% { opacity: 1; } 85% { opacity: 0.15; } 100% { opacity: 1; } }

/* GM-only controls (hide/reveal, delete) -- a small row above the marker,
   same shape as .stamp-controls/.character-icons elsewhere, rather than
   individually-positioned corner buttons: a fixed row reads consistently
   whether the marker underneath it is the normal 34px size or the
   "scenario" icon's doubled 68px, with no per-size repositioning needed. */
.map-icon-controls {
  position: absolute;
  top: -24px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 2px;
  background: rgba(20, 15, 8, 0.78);
  border-radius: 6px;
  padding: 2px 4px;
  white-space: nowrap;
}
/* Small, always-visible (not just GM) indicator that an icon has something
   behind it worth double-clicking -- an Organization "launcher" (📋) or a
   linked Scenario "portal" (🧭); a given icon only ever shows one or the
   other, never both, so sharing one corner position is safe. Deliberately
   just a passive badge, not a button: the only way to act on either link
   is to double-click the icon itself. */
.map-icon-linked-badge {
  position: absolute;
  bottom: -6px;
  right: -6px;
  font-size: 0.65rem;
  line-height: 1;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8));
  pointer-events: none;
}

/* A Stamp is deliberately rendered ABOVE every other entity on the canvas
   (the whole point of the feature -- see the Stamp model docstring in
   models.py) via a z-index far higher than anything else on the board
   (the next-highest canvas-content z-index anywhere in this file is 5).
   Sized to STAMP_DISPLAY_W/H in canvas.js; keep the three in sync.
   `opacity` is bound inline per-instance from stampFadeOpacity() rather
   than animated in CSS, since the fade is driven by real elapsed wall-clock
   time (see startStampTicking() in canvas.js) not a fixed-duration CSS
   animation -- the `transition` below just smooths the once-a-second jumps
   between ticks into something that reads as continuous. */
.stamp-marker {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: grab;
  touch-action: none;
  user-select: none;
  z-index: 1000;
  transition: opacity 1s linear;
}
.stamp-glyph {
  display: block;
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.6));
}
.stamp-glyph svg { display: block; width: 100%; height: 100%; }
.stamp-controls {
  position: absolute;
  top: -26px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 2px;
  background: rgba(20, 15, 8, 0.78);
  border-radius: 6px;
  padding: 2px 4px;
  white-space: nowrap;
}
/* Who placed this stamp -- see Stamp.created_by's docstring, models.py.
   Positioned below the glyph (stamp-controls sits above it, at top:
   -26px) so the two never collide. Small and subdued -- this is
   attribution, not a label meant to compete with the stamp graphic
   itself for attention -- but still legible against any part of the
   map behind it, hence the solid dark chip rather than plain text. */
.stamp-creator-label {
  position: absolute;
  bottom: -22px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(20, 15, 8, 0.78);
  color: var(--parchment);
  font-family: 'Cinzel', serif;
  font-size: 0.7rem;
  padding: 1px 6px;
  border-radius: 6px;
  white-space: nowrap;
  pointer-events: none;
}

.route-curve-toggle {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  color: var(--ink-soft);
}
.route-curve-toggle input[type="checkbox"] { width: 16px; height: 16px; }

/* Pin-a-relation-endpoint-to-the-map row -- see canPinEndpoint()/
   toggleEndpointPin() in canvas.js and the Route model docstring. */
.route-pin-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  color: var(--ink-soft);
}
.route-pin-label { flex: 1; }
.route-pin-status { display: flex; align-items: center; gap: 0.5rem; }

.character-field-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-family: 'Cinzel', serif;
  font-size: 0.8rem;
  color: var(--ink-soft);
}
.character-field-label input,
.character-field-label select,
.character-field-label textarea {
  font-family: inherit;
  font-size: 0.9rem;
  padding: 0.4rem 0.5rem;
  border-radius: 6px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}
.character-field-label textarea { min-height: 80px; resize: vertical; }

/* ---------- Organization cards (reuse the character-card token look) ---------- */

.organization-card { border-color: var(--danger-bright); }
.organization-card .character-plate { background: linear-gradient(0deg, rgba(40, 10, 8, 0.92), rgba(40, 10, 8, 0.05)); }
.organization-card .character-faction { color: #e79a86; }

/* ---------- Place cards ---------- */

.place-card {
  position: absolute;
  width: 200px;
  height: 200px;
  background: linear-gradient(180deg, #e7ecd8, #cdd6b3);
  border: 2px solid var(--ok);
  border-radius: 8px;
  box-shadow: 0 6px 16px var(--shadow);
  color: var(--ink);
  cursor: grab;
  overflow: hidden;
  touch-action: none;
  user-select: none;
}
.place-card.note-editing { border-color: var(--gold-bright); box-shadow: 0 0 0 3px rgba(240, 205, 122, 0.4), 0 6px 16px var(--shadow); cursor: default; }
.place-display { padding: 0.5rem 0.6rem; height: calc(100% - 26px); display: flex; flex-direction: column; }
.place-icon { font-size: 1.3rem; margin-bottom: 0.15rem; }

/* ---------- Frames (background grouping rectangles) ----------
   Deliberately behind everything else: rendered first in the template so
   every other entity and the route lines paint on top of them without any
   z-index bookkeeping. Free-form width/height (not the fixed *_SIZE constants
   the other card types use), resized by dragging any of the four corner
   handles independently. The handles only show once a frame is selected (a
   plain click, not a drag) -- at rest its corners carry a small decorative
   fleuron instead, echoing the flourish glyphs on Label cards. */

.frame-box {
  position: absolute;
  border: 6px double var(--gold-dim);
  border-radius: 12px;
  background: rgba(201, 162, 75, 0.07);
  cursor: grab;
  touch-action: none;
  user-select: none;
  box-sizing: border-box;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.frame-box.frame-selected {
  border-color: var(--gold-bright);
  box-shadow: 0 0 0 3px rgba(240, 205, 122, 0.4), 0 6px 16px var(--shadow);
}
/* Collapsed state (toggled via toggleFrameMinimized, canvas.js) -- the
   frame's own height is forced to 40px inline (see the :style binding
   in scenario.html), so this just needs to keep whatever would
   otherwise overflow that short a box out of view, and give the "📦 N
   items" label somewhere to sit. A more solid background than the
   frame's usual faint tint, since that label needs to actually be
   readable against it rather than blending into the map. */
.frame-box.frame-minimized {
  overflow: hidden;
  background: rgba(44, 34, 19, 0.82);
  display: flex;
  align-items: center;
  justify-content: center;
}
.frame-minimized-label {
  font-family: 'Cinzel', serif;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--gold-bright);
  letter-spacing: 0.02em;
  pointer-events: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: calc(100% - 2rem);
}
.frame-icons {
  position: absolute;
  top: -0.9rem;
  left: 0.5rem;
  display: flex;
  gap: 2px;
  background: rgba(20, 15, 8, 0.55);
  border-radius: 6px;
  padding: 2px 4px;
  opacity: 0.35;
  transition: opacity 0.15s ease;
}
.frame-box:hover .frame-icons { opacity: 1; }
.frame-corner-ornament {
  position: absolute;
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: serif;
  font-size: 1rem;
  line-height: 1;
  color: var(--gold-bright);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
  pointer-events: none;
  user-select: none;
}
.frame-corner-nw { top: -11px; left: -11px; }
.frame-corner-ne { top: -11px; right: -11px; }
.frame-corner-sw { bottom: -11px; left: -11px; }
.frame-corner-se { bottom: -11px; right: -11px; }
.frame-handle {
  position: absolute;
  width: 14px;
  height: 14px;
  background: var(--gold-bright);
  border: 2px solid var(--ink);
  border-radius: 3px;
  touch-action: none;
}
.frame-handle-nw { top: -8px; left: -8px; cursor: nwse-resize; }
.frame-handle-se { bottom: -8px; right: -8px; cursor: nwse-resize; }
.frame-handle-ne { top: -8px; right: -8px; cursor: nesw-resize; }
.frame-handle-sw { bottom: -8px; left: -8px; cursor: nesw-resize; }

/* Note's own corner-resize positions -- shares .frame-handle's visual
   style, but sits just INSIDE the box edge (no negative offset) since
   .note-box has overflow:hidden (needed to contain note text/thumbnails),
   which would clip a handle straddling the edge the way Frame's does. */
.note-handle-nw { top: 2px; left: 2px; cursor: nwse-resize; }
.note-handle-se { bottom: 2px; right: 2px; cursor: nwse-resize; }
.note-handle-ne { top: 2px; right: 2px; cursor: nesw-resize; }
.note-handle-sw { bottom: 2px; left: 2px; cursor: nesw-resize; }

/* ---------- Scenario background image ---------- */
/* Deliberately plain (no border/no icons) so it reads as a map, not a UI
   element, until a GM selects it -- resize handles reuse .frame-handle,
   the same corner-drag affordance Frame already uses. */
.background-image {
  position: absolute;
  background-size: 100% 100%;
  background-repeat: no-repeat;
  background-position: center;
  box-sizing: border-box;
  cursor: grab;
  touch-action: none;
  user-select: none;
  border: 2px solid transparent;
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.background-image.background-selected {
  border-color: var(--gold-bright);
  box-shadow: 0 0 0 3px rgba(240, 205, 122, 0.4), 0 6px 16px var(--shadow);
}

/* ---------- Label cards (no background -- just fancy floating text) ---------- */

.label-card {
  position: absolute;
  width: 340px;
  min-height: 90px;
  cursor: grab;
  touch-action: none;
  user-select: none;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.4rem 0;
}
.label-icons {
  position: absolute;
  top: -0.3rem;
  right: 0.4rem;
  display: flex;
  gap: 2px;
  background: rgba(20, 15, 8, 0.55);
  border-radius: 6px;
  padding: 2px 4px;
  opacity: 0.35;
  transition: opacity 0.15s ease;
}
.label-card:hover .label-icons { opacity: 1; }
.label-text {
  font-family: 'Cinzel Decorative', 'Cinzel', serif;
  font-weight: 700;
  font-size: 1.9rem;
  line-height: 1.15;
  color: var(--gold-bright);
  text-align: center;
  padding: 0 0.5rem;
  max-height: 100%;
  overflow: hidden;
  word-break: break-word;
  text-shadow:
    0 0 14px rgba(0, 0, 0, 0.85),
    0 2px 0 #000,
    0 0 26px rgba(240, 205, 122, 0.45);
  letter-spacing: 0.02em;
}
.label-text::before,
.label-text::after {
  content: '\2766';
  display: block;
  font-family: serif;
  font-weight: 400;
  font-size: 1rem;
  color: var(--gold-dim);
  text-shadow: 0 1px 3px #000;
  margin: 0.15rem 0;
}
.label-text-input {
  font-family: 'Cinzel Decorative', 'Cinzel', serif;
  font-size: 1.4rem;
  text-align: center;
  width: 85%;
  padding: 0.4rem 0.6rem;
  border-radius: 8px;
  border: 1px solid var(--gold-dim);
  background: rgba(20, 15, 8, 0.85);
  color: var(--gold-bright);
}

.note-edit-fields { padding: 0.5rem 0.5rem; height: calc(100% - 26px); display: flex; flex-direction: column; gap: 0.4rem; }
.note-label-input {
  font-family: 'Cinzel', serif;
  font-size: 0.85rem;
  padding: 0.3rem 0.4rem;
  border-radius: 4px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}
.note-text-input {
  flex: 1;
  resize: none;
  font-family: inherit;
  font-size: 0.8rem;
  padding: 0.35rem 0.4rem;
  border-radius: 4px;
  border: 1px solid var(--ink-soft);
  background: #fbf6e6;
  color: var(--ink);
}

@media (max-width: 720px) {
  .toolbar { flex-direction: column; align-items: stretch; }
  .toolbar-left, .toolbar-right { justify-content: space-between; }
  .presentation-stage { height: min(90vh, 900px); }
  .presentation-header { align-items: flex-start; }
  .presentation-actions { flex-direction: column; }
}
