/* ==========================================================================
   Ivalice — an interactive gazetteer
   Plain CSS, no build step. Edit freely; nothing here is generated.

   THEMING
   app.js always writes data-theme="light" or "dark" onto <html> (an inline
   script in <head> does it before first paint, so there's no flash). That means
   the two palettes below are the only place colour lives — no duplicated
   prefers-color-scheme blocks to keep in sync.

   `color-scheme: only light` on the light theme is load-bearing: plain `light`
   still lets Chrome's auto-dark-mode repaint the page, which turns a parchment
   map into an unreadable negative. The dark theme declares `dark`, which also
   stops Chrome force-darkening it — an already-dark page is left alone.
   ========================================================================== */

:root {
  --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
           Georgia, "Times New Roman", serif;
  /* Place labels. The reference sets these in a humanist sans, but a sans
     reads cold and modern against aged parchment — the owner asked for
     something bookish and scholarly, so they take the same old-style serif as
     the rest of the site. A heavier weight and a stronger shadow make up for
     the thinner strokes over busy ground. */
  --label-serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
                 "Hoefler Text", Georgia, "Times New Roman", serif;
  --ease: cubic-bezier(0.2, 0, 0, 1);

  /* Marker hit target — written by app.js, never exceeds the guaranteed
     spacing between two markers, so touch targets can't overlap. */
  --hit: 44px;
}

/* ------------------------------------------------------ light: daylight map */

:root,
:root[data-theme="light"] {
  color-scheme: only light;

  --paper:        #e9dcba;         /* topbar text — the bar is dark in both themes */
  --surface:      #efe5c8;         /* panel, results, search dropdown */
  --surface-veil: rgba(239, 229, 200, 0.9);
  --label-halo:   rgba(230, 212, 170, 0.9);
  --on-seal:      #efe5c8;         /* text/glyph sitting on the seal colour */

  --sea:        #a0a893;
  --land:       #dcc086;
  --land-cold:  #ded3b4;
  --aloft:      #4a5648;
  --coast:      #5c4526;
  --mottle:     #8a6a33;
  --crackle:    #4a3718;
  --mottle-op:  1;
  --crackle-op: 0.85;
  --marble-a:   #8a6524;
  --marble-a-op: 0.38;
  --marble-b:   #5e4519;
  --marble-b-op: 0.26;
  --marble-sea: #3f4d42;
  --marble-sea-op: 0.3;
  --speckle:    #4a3a1c;
  --speckle-op: 0.3;
  --rule-ink:   #4e3d20;
  --rule-op:    0.16;
  --map-label:  #46341c;
  --map-label-sea: #4a5850;
  --river:      #7b8a86;
  --grain:      #6b5836;

  /* Terrain washes — the ground's colour range, not region swatches. */
  --wash-sand:  #f2d179;
  --wash-grass: #dcc27c;
  --wash-wood:  #96a06a;
  --wash-pale:  #cdd0c2;
  --wash-dead:  #b0a184;
  --wash-stone: #c8b592;
  --wash-rust:  #b6813f;
  --wash-cream: #faf0d4;
  --wash-shade: #4e3d22;

  --contour-land: #7a5f34;
  --contour-sea:  #d5dbca;
  --script-ink:   #5a4526;
  --sea-deep:     #6f7d69;
  --sea-shallow:  #c3cbb4;
  --shoal:        #5f6b58;         /* water darkening as it meets the shore */
  --shore:        #efdcae;         /* the pale band just inside the coast */
  --road:         #f6ecd2;
  --road-shadow:  #3a2c16;

  --ct-fill:      #dcc394;
  --ct-fill-dark: #c2a878;
  --ct-line:      #4e3718;
  --bd-line:      #5c4526;
  --vig-op:   1;
  --stain-op: 1;

  --ink:        #33281a;
  --ink-soft:   rgba(51, 40, 26, 0.64);
  --ink-faint:  rgba(51, 40, 26, 0.38);
  --ink-body:   rgba(51, 40, 26, 0.9);
  --ink-body-2: rgba(51, 40, 26, 0.86);

  --tint-06: rgba(51, 40, 26, 0.06);
  --tint-07: rgba(51, 40, 26, 0.07);
  --tint-10: rgba(51, 40, 26, 0.10);
  --tint-12: rgba(51, 40, 26, 0.12);
  --tint-18: rgba(51, 40, 26, 0.18);
  --tint-22: rgba(51, 40, 26, 0.22);
  --rule-a:  rgba(51, 40, 26, 0.22);
  --rule-b:  rgba(51, 40, 26, 0.08);

  --seal:             #8d3a25;     /* imperial red — selection */
  --seal-ring:        rgba(141, 58, 37, 0.4);
  --seal-ring-strong: rgba(141, 58, 37, 0.9);
  --gold:             #9c7628;

  /* Place photographs. They're bright daylight screenshots and the panel is
     not, so they get pulled toward the sheet rather than dropped onto it.
     Tokens, not constants, because the dark panel needs much more of it. */
  --photo-dim: 0.97;
  --photo-sat: 0.94;

  /* Markers. The map is deliberately desaturated, so these accents are the
     only saturated colour on the sheet and carry the whole place hierarchy. */
  --glyph-body:   #f7efd8;
  --glyph-shade:  #d9cbaa;
  --glyph-roof:   #8f4632;
  --glyph-edge:   rgba(38, 28, 14, 0.85);
  --mk-city:      #2f7f8c;
  --mk-fortress:  #3a5f86;
  --mk-dungeon:   #4a5566;
  --mk-wild:      #5c8f45;
  --mk-sacred:    #9c7628;
  --mk-sky:       #57b0bd;
  --mk-label:     #fdf6e6;
  --mk-label-shadow:
    0 1px 1px rgba(24, 18, 8, 0.98),
    0 2px 6px rgba(24, 18, 8, 0.7),
    0 0 3px rgba(24, 18, 8, 0.9),
    0 0 7px rgba(24, 18, 8, 0.5);

  --surround: #211d16;
  --topbar-a: #2a251b;
  --topbar-b: #211d16;

  /* Elevation. Shadows for depth, borders only for structure. */
  --shadow-border:
    0 0 0 1px oklch(0 0 0 / 0.08),
    0 1px 2px -1px oklch(0 0 0 / 0.10),
    0 2px 4px 0 oklch(0 0 0 / 0.06);
  --shadow-border-hover:
    0 0 0 1px oklch(0 0 0 / 0.12),
    0 1px 2px -1px oklch(0 0 0 / 0.14),
    0 2px 4px 0 oklch(0 0 0 / 0.09);
  --shadow-panel:
    0 0 0 1px oklch(0 0 0 / 0.10),
    0 2px 6px -2px oklch(0 0 0 / 0.14),
    0 12px 32px -8px oklch(0 0 0 / 0.26),
    0 32px 64px -16px oklch(0 0 0 / 0.22);
}

/* ----------------------------------------------------- dark: candlelit map */

:root[data-theme="dark"] {
  color-scheme: dark;

  --paper:        #e9dcba;
  --surface:      #262119;
  --surface-veil: rgba(38, 33, 25, 0.92);
  --label-halo:   rgba(26, 22, 16, 0.9);
  --on-seal:      #17140f;

  --sea:        #141b1b;
  --land:       #3a3225;
  --land-cold:  #363328;
  --aloft:      #000000;
  --coast:      #a68b58;
  --mottle:     #c9a468;
  --crackle:    #e0c894;
  --mottle-op:  0.4;
  --crackle-op: 0.26;
  --marble-a:   #cfa964;
  --marble-a-op: 0.26;
  --marble-b:   #e8cf9a;
  --marble-b-op: 0.16;
  --marble-sea: #7d9a90;
  --marble-sea-op: 0.14;
  --speckle:    #e8d5a8;
  --speckle-op: 0.14;
  --rule-ink:   #d8c290;
  --rule-op:    0.13;
  --map-label:  #cdb488;
  --map-label-sea: #8fa39a;
  --river:      #6d8480;
  --grain:      #d8c290;

  /* Same job as the light washes: the range the ground moves through. Kept
     warmer and a touch lighter than the old dark palette, which read muddy. */
  --wash-sand:  #5c4a2a;
  --wash-grass: #464526;
  --wash-wood:  #33421f;
  --wash-pale:  #454b46;
  --wash-dead:  #423d33;
  --wash-stone: #4a4232;
  --wash-rust:  #7a4f21;
  --wash-cream: #6d5f3e;
  --wash-shade: #0c0906;

  --contour-land: #c2a66c;
  --contour-sea:  #7d9a90;
  --script-ink:   #c6ac7a;
  --sea-deep:     #080d0d;
  --sea-shallow:  #2c3a37;
  --shoal:        #05090a;
  --shore:        #6b5836;
  --road:         #f0dfae;
  --road-shadow:  #000000;

  --ct-fill:      #3a3123;
  --ct-fill-dark: #2c2419;
  --ct-line:      #c9ac70;
  --bd-line:      #8a7346;
  --vig-op:   0.55;
  --stain-op: 0.4;

  --ink:        #e9dcc0;
  --ink-soft:   rgba(233, 220, 192, 0.66);
  --ink-faint:  rgba(233, 220, 192, 0.42);
  --ink-body:   rgba(233, 220, 192, 0.88);
  --ink-body-2: rgba(233, 220, 192, 0.84);

  --tint-06: rgba(233, 220, 192, 0.07);
  --tint-07: rgba(233, 220, 192, 0.08);
  --tint-10: rgba(233, 220, 192, 0.12);
  --tint-12: rgba(233, 220, 192, 0.14);
  --tint-18: rgba(233, 220, 192, 0.2);
  --tint-22: rgba(233, 220, 192, 0.24);
  --rule-a:  rgba(233, 220, 192, 0.26);
  --rule-b:  rgba(233, 220, 192, 0.1);

  --seal:             #c2593c;
  --seal-ring:        rgba(194, 89, 60, 0.5);
  --seal-ring-strong: rgba(194, 89, 60, 0.95);
  --gold:             #d3a75a;

  /* A daylight screenshot at full brightness on the candlelit panel is a
     lightbulb. This is the same reasoning as --mottle-op: the pass inverts. */
  --photo-dim: 0.82;
  --photo-sat: 0.88;

  /* Brighter accents and a dark glyph body — a light body would glow against
     the candlelit ground instead of sitting on it. */
  --glyph-body:   #e7dcc0;
  --glyph-shade:  #b9ab8c;
  --glyph-roof:   #8a4634;
  --glyph-edge:   rgba(10, 8, 5, 0.9);
  --mk-city:      #3f9dab;
  --mk-fortress:  #5b86b5;
  --mk-dungeon:   #6b788c;
  --mk-wild:      #7cb356;
  --mk-sacred:    #c39a4e;
  --mk-sky:       #6fc9d6;
  --mk-label:     #f4ead4;
  --mk-label-shadow:
    0 1px 1px rgba(0, 0, 0, 1),
    0 2px 6px rgba(0, 0, 0, 0.85),
    0 0 3px rgba(0, 0, 0, 0.95),
    0 0 8px rgba(0, 0, 0, 0.6);

  --surround: #0c0b09;
  --topbar-a: #1b1813;
  --topbar-b: #121009;

  /* On dark, layered depth shadows aren't visible — simplify to a light ring. */
  --shadow-border:       0 0 0 1px oklch(1 0 0 / 0.08);
  --shadow-border-hover: 0 0 0 1px oklch(1 0 0 / 0.13);
  --shadow-panel:
    0 0 0 1px oklch(1 0 0 / 0.09),
    0 12px 32px -8px oklch(0 0 0 / 0.6),
    0 32px 64px -16px oklch(0 0 0 / 0.5);
}

* { box-sizing: border-box; }

html {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--serif);
  color: var(--ink);
  background: var(--surround);
  overflow: hidden;
  overscroll-behavior: none;
}

h1, h2, h3 { text-wrap: balance; margin: 0; font-weight: 600; }
p { text-wrap: pretty; margin: 0; }

:focus-visible {
  outline: 2px solid var(--seal);
  outline-offset: 3px;
  border-radius: 4px;
}

/* The panel takes programmatic focus when opened from a deep link, so screen
   readers land on it. It isn't a control, and its entrance animation is already
   unmistakable, so it doesn't need a focus ring drawn round the whole card. */
.panel:focus, .panel:focus-visible { outline: none; }

/* --------------------------------------------------------------- app shell */

.app {
  position: fixed;
  inset: 0;
  display: grid;
  grid-template-rows: auto 1fr;
}

/* ------------------------------------------------------------------ topbar */

.topbar {
  position: relative;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 12px 18px;
  background: linear-gradient(180deg, var(--topbar-a), var(--topbar-b));
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.06), 0 6px 20px -8px rgba(0, 0, 0, 0.7);
  transition-property: background;
  transition-duration: 260ms;
  transition-timing-function: var(--ease);
}

.brand { display: flex; align-items: baseline; gap: 12px; min-width: 0; }

.brand h1 {
  font-size: 19px;
  letter-spacing: 0.34em;
  text-indent: 0.34em;      /* letter-spacing adds trailing space; recentre */
  color: var(--paper);
  font-weight: 500;
  white-space: nowrap;
}

.brand .tagline {
  font-size: 12.5px;
  color: rgba(233, 220, 186, 0.5);
  font-style: italic;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.topbar-tools { margin-left: auto; display: flex; align-items: center; gap: 10px; }

/* ------------------------------------------------------------------ search */

.search {
  position: relative;
  display: flex;
  align-items: center;
}

.search svg {
  position: absolute;
  left: 10px;
  width: 15px;
  height: 15px;
  color: rgba(233, 220, 186, 0.45);
  pointer-events: none;
}

.search input {
  font: inherit;
  font-size: 14px;
  width: 190px;
  padding: 8px 12px 8px 31px;   /* icon side trimmed for optical balance */
  border: 0;
  border-radius: 9px;
  color: var(--paper);
  background: rgba(233, 220, 186, 0.09);
  box-shadow: inset 0 0 0 1px rgba(233, 220, 186, 0.14);
  transition-property: box-shadow, background-color, width;
  transition-duration: 160ms;
  transition-timing-function: var(--ease);
}

.search input::placeholder { color: rgba(233, 220, 186, 0.38); }
.search input:hover { background: rgba(233, 220, 186, 0.13); }

.search input:focus {
  outline: none;
  width: 230px;
  background: rgba(233, 220, 186, 0.16);
  box-shadow: inset 0 0 0 1px rgba(233, 220, 186, 0.34);
}

/* --------------------------------------------------------------- controls */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: 34px;
  padding: 0 11px;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(233, 220, 186, 0.78);
  background: rgba(233, 220, 186, 0.08);
  border: 0;
  border-radius: 9px;
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(233, 220, 186, 0.13);
  transition-property: background-color, color, box-shadow, scale;
  transition-duration: 140ms;
  transition-timing-function: var(--ease);
}

.btn:hover { background: rgba(233, 220, 186, 0.15); color: var(--paper); }
.btn:active { scale: 0.96; }

.btn svg { width: 15px; height: 15px; }

.btn-icon {
  width: 34px;
  padding: 0;
  font-size: 15px;
}

.zoom {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 12px;                 /* 9 + 3 padding — concentric */
  background: rgba(0, 0, 0, 0.22);
  box-shadow: inset 0 0 0 1px rgba(233, 220, 186, 0.10);
}

.zoom-readout {
  min-width: 48px;
  text-align: center;
  font-size: 11.5px;
  letter-spacing: 0.06em;
  color: rgba(233, 220, 186, 0.55);
  font-variant-numeric: tabular-nums;  /* no reflow as the number changes */
}

/* ------------------------------------------------------------ theme toggle */

/* Both icons stay in the DOM, one stacked on the other, and cross-fade — so
   the outgoing icon animates out as well as the incoming one animating in,
   with no motion library. */
.icon-swap {
  position: relative;
  width: 15px;
  height: 15px;
  display: grid;
  place-items: center;
}

.icon-swap .i {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  transition-property: opacity, filter, scale;
  transition-duration: 300ms;
  transition-timing-function: var(--ease);
}

.icon-swap .i svg { width: 15px; height: 15px; }

/* Show the icon for the theme you'd switch TO. */
:root[data-theme="light"] .i-sun  { opacity: 0; scale: 0.25; filter: blur(4px); }
:root[data-theme="light"] .i-moon { opacity: 1; scale: 1;    filter: blur(0); }
:root[data-theme="dark"]  .i-sun  { opacity: 1; scale: 1;    filter: blur(0); }
:root[data-theme="dark"]  .i-moon { opacity: 0; scale: 0.25; filter: blur(4px); }

/* ------------------------------------------------------------------ legend */

.legend {
  position: absolute;
  z-index: 30;
  left: 16px;
  bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 9px;
  border-radius: 17px;                 /* 8 + 9 padding — concentric */
  background: var(--surface-veil);
  -webkit-backdrop-filter: blur(8px);   /* Safari wants the prefix before 18 */
  backdrop-filter: blur(8px);
  box-shadow: var(--shadow-panel);
}

.legend-title {
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 2px 8px 6px;
}

.legend button {
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 30px;
  padding: 0 10px 0 8px;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 8px;
  cursor: pointer;
  text-align: left;
  transition-property: background-color, opacity;
  transition-duration: 120ms;
  transition-timing-function: var(--ease);
}

.legend button:hover { background: var(--tint-07); }

/* A check box, not a colour key — the map doesn't tint landmasses by
   continent, so a coloured swatch would imply a legend that isn't there. */
.legend .swatch {
  width: 15px;
  height: 15px;
  border-radius: 4px;
  flex: none;
  display: grid;
  place-items: center;
  color: var(--on-seal);
  background: var(--seal);
  box-shadow: inset 0 0 0 1px var(--tint-22);
  transition-property: background-color, color;
  transition-duration: 120ms;
  transition-timing-function: var(--ease);
}

.legend .swatch svg { width: 10px; height: 10px; }

.legend button[aria-pressed="false"] { opacity: 0.5; }
.legend button[aria-pressed="false"] .swatch { background: transparent; color: transparent; }

.legend .count {
  margin-left: auto;
  font-size: 11px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* -------------------------------------------------------------- map canvas */

.viewport {
  position: relative;
  overflow: hidden;
  background: var(--surround);
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.viewport.is-dragging { cursor: grabbing; }

#map-art {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  transform: translate(calc(var(--tx) * 1px), calc(var(--ty) * 1px)) scale(var(--z));
  will-change: transform;              /* the one element that earns it */
  pointer-events: none;
}

/* ---- map SVG paint -------------------------------------------------------
   Every value themed, so one generated artwork serves both palettes.
   Stroke widths are in map units (the whole SVG is scaled by --z). */

.m-sea       { fill: var(--sea); }
.m-land      { fill: var(--land); }
.land-valendia { fill: var(--land); }
.land-isle     { fill: var(--land); }

/* Terrain colour. Each region is a big soft blob, blurred hard and clipped to
   the land, so the ground runs cream to rust across the sheet rather than
   sitting at one flat tint. Nothing here has an edge you can point at. */
.m-wash .w-sand  { fill: var(--wash-sand); }
.m-wash .w-grass { fill: var(--wash-grass); }
.m-wash .w-wood  { fill: var(--wash-wood); }
.m-wash .w-pale  { fill: var(--wash-pale); }
.m-wash .w-dead  { fill: var(--wash-dead); }
.m-wash .w-stone { fill: var(--wash-stone); }

/* The staining. These carry most of the ground's luminance range, so they run
   at high opacity — the reference's land swings much further between light and
   dark than any single terrain tint would explain. */
.m-wash .w-rust  { fill: var(--wash-rust);  opacity: 0.62; }
.m-wash .w-cream { fill: var(--wash-cream); opacity: 0.6; }
.m-wash .w-shade { fill: var(--wash-shade); opacity: 0.46; }

.m-wash-sea .w-deep    { fill: var(--sea-deep);    opacity: 0.55; }
.m-wash-sea .w-shallow { fill: var(--sea-shallow); opacity: 0.45; }

/* Blotchy tonal variation across the ground, so it isn't a flat fill. Both
   passes invert per theme — dark ink over pale parchment, pale over dark — so
   their strength has to be a token too. At the light theme's amplitude the
   dark map's ground washed out to pale grey. */
.m-mottle    { fill: var(--mottle);  opacity: var(--mottle-op); }
.m-crackle   { fill: var(--crackle); opacity: var(--crackle-op); }

/* Marbling and dust. These are what make the ground read as a printed, aged
   sheet rather than an airbrushed gradient, and they were the thing missing
   when the map still looked soft next to the reference. Blurred washes and
   crisp texture can have identical colour histograms — the difference is
   entirely spatial frequency, so it has to be drawn, not blurred. */
.m-marble-a  { fill: var(--marble-a); opacity: var(--marble-a-op); }
.m-marble-b  { fill: var(--marble-b); opacity: var(--marble-b-op); }
.m-marble-sea { fill: var(--marble-sea); opacity: var(--marble-sea-op); }
.m-speckle   { fill: var(--speckle); opacity: var(--speckle-op); }

/* Bearing lines, fold creases and a sparse graticule. Almost invisible one at
   a time; collectively a large part of why a chart reads as a chart. */
.m-rules     { fill: none; stroke: var(--rule-ink); stroke-width: 1.5; opacity: var(--rule-op); }
.m-graticule { fill: none; stroke: var(--rule-ink); stroke-width: 1.1; opacity: calc(var(--rule-op) * 0.62); }

/* Relief as nested contour loops — the survey-chart reading of terrain, and
   the reason the map needs no drawn mountains or forest. */
.m-contour       { fill: none; stroke-width: 1.8; }
.m-contour-land  { stroke: var(--contour-land); opacity: 0.5; }
.m-contour-sea   { stroke: var(--contour-sea);  opacity: 0.48; }

/* Annotation in an invented hand. Deliberately at the threshold of legibility:
   it should register as writing and never resolve into letters. */
.m-script    { fill: none; stroke: var(--script-ink); stroke-width: 1.6;
               stroke-linecap: round; stroke-linejoin: round; opacity: 0.36; }

/* The coastline, in three parts. A dark band in the water and a pale band on
   the land do the actual work — the boundary is a tonal step, not a drawn
   line. The hairline is insurance: at low zoom the tonal edge alone leaves the
   shape of Ivalice hard to read. */
.m-aloft     { fill: var(--aloft); opacity: 0.34; }
.m-shoal     { fill: none; stroke: var(--shoal); stroke-width: 13; opacity: 0.5; }
.m-shore     { fill: none; stroke: var(--shore); stroke-width: 15; opacity: 0.45; }
.m-coast     { fill: none; stroke: var(--coast); stroke-width: 1.6; stroke-linejoin: round;
               opacity: 0.42; }

/* Kerwon's snowfield — a soft tint inside the shared coastline, so the cold
   region reads without implying a separate island. */
.m-snow      { fill: var(--land-cold); opacity: 0.55; }

.m-river     { fill: none; stroke: var(--river); stroke-width: 3.4; stroke-linecap: round; opacity: 0.6; }

/* Trade routes: a soft dark pass, then a bright cream line over it. */
.m-road-shadow { fill: none; stroke: var(--road-shadow); stroke-width: 6.5;
                 stroke-linecap: round; opacity: 0.45; }
.m-road        { fill: none; stroke: var(--road); stroke-width: 2.4;
                 stroke-linecap: round; opacity: 0.85; }


/* Halo behind every map label: a fat stroke in the ground colour painted
   underneath the fill. Standard cartographic trick — guarantees the name reads
   over mountains, forest and stipple without a solid plate behind it. */
.m-label     { fill: var(--map-label); font-family: var(--serif); letter-spacing: 0.05em;
               stroke: var(--label-halo); stroke-width: 10; stroke-linejoin: round;
               paint-order: stroke fill; }
.m-label .lb-sea  { fill: var(--map-label-sea); font-style: italic; }
.m-label .lb-cont { font-weight: 600; }

/* Title cartouche — the ribbon in the top-left corner. */
.ct-body     { fill: var(--ct-fill); stroke: var(--ct-line); stroke-width: 4; }
.ct-tail     { fill: var(--ct-fill-dark); stroke: var(--ct-line); stroke-width: 4; }
.ct-rule     { fill: none; stroke: var(--ct-line); stroke-width: 2.2; opacity: 0.55; }
.ct-title    { fill: var(--ct-line); stroke: var(--ct-fill); stroke-width: 5; stroke-linejoin: round; paint-order: stroke fill; font-family: var(--serif); font-weight: 600; letter-spacing: 0.04em; }

/* Ruled frame around the whole sheet. */
.bd-out      { fill: none; stroke: var(--bd-line); stroke-width: 6; }
.bd-in       { fill: none; stroke: var(--bd-line); stroke-width: 2.2; opacity: 0.6; }

/* Ageing passes, painted last. */
.m-vig       { opacity: var(--vig-op); }
.m-stain     { opacity: var(--stain-op); }
.m-grainlayer rect { fill: var(--grain); }

/* ----------------------------------------------------------------- markers */

.marker-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.marker {
  position: absolute;
  top: 0;
  left: 0;
  /* The marker box is exactly the glyph. The label is positioned out of flow
     beside it, so it can never contribute to layout — which is what made every
     marker sit off its coordinate the first time round (pitfall 1), and what
     would otherwise make flipping a label to the left shift the pin. */
  display: block;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--ink);
  font: inherit;

  /* Anchor the GLYPH on the coordinate, not the glyph+label pair — the label
     is a tail hanging off the pin, and it must not drag the pin sideways.
     (Hidden labels still take layout space, so this affects every marker.) */
  transform:
    translate(calc(var(--tx) * 1px + var(--x) * var(--z) * 1px),
              calc(var(--ty) * 1px + var(--y) * var(--z) * 1px))
    translate(-13px, -13px);

  /* Clickable region is the ::after disc and nothing else. Without this the
     button's box — including an invisible label — would be hit-testable, and
     neighbouring targets would overlap however carefully the coords are spaced. */
  pointer-events: none;

  transition-property: opacity, filter;
  transition-duration: 180ms;
  transition-timing-function: var(--ease);
}

/* Touch target, centred on the glyph. Sized by --hit, which app.js keeps at or
   below the guaranteed marker separation so two can never overlap. */
.marker::after {
  content: "";
  position: absolute;
  top: 13px;
  left: 13px;
  width: var(--hit);
  height: var(--hit);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  pointer-events: auto;
}

/* The glyph is a filled body in the paper colour with a saturated accent
   outline, plus a cast shadow — a small object sitting on the sheet. The disc
   that used to sit behind it is gone: it existed to separate the marker from
   the tree and dune glyphs scattered over the old artwork, and there are no
   longer any of those to compete with. */
.marker-glyph {
  position: relative;
  width: 26px;
  height: 26px;
  flex: none;
  display: grid;
  place-items: center;
  color: var(--accent);
  scale: calc(var(--gbase) * var(--gmul));
  pointer-events: auto;
  transition-property: color, scale;
  transition-duration: 130ms;
  transition-timing-function: var(--ease);
}

.marker-glyph svg {
  position: relative;
  width: 26px;
  height: 26px;
  overflow: visible;
  filter: drop-shadow(0 1.5px 2px rgba(20, 14, 6, 0.6));
}

/* Icon parts. The plinth carries the per-kind accent — it is the only
   saturated colour in the marker, exactly as on the reference map, where a
   cream building sits on a bright cyan-and-green base. */
.ic-plinth      { fill: var(--accent); }
.ic-plinth-side { fill: var(--accent); filter: brightness(0.72); }
.ic-body        { fill: var(--glyph-body); }
.ic-shade       { fill: var(--glyph-shade); }
.ic-roof        { fill: var(--glyph-roof); }
.marker-glyph svg > * { stroke: var(--glyph-edge); stroke-width: 0.9; stroke-linejoin: round; }

/* Accent per kind. These are the only saturated colours on the map, so they
   have to carry the hierarchy that 38 identical discs previously flattened. */
.marker { --accent: var(--mk-wild); }
.marker[data-type="city"]     { --accent: var(--mk-city); }
.marker[data-type="fortress"] { --accent: var(--mk-fortress); }
.marker[data-type="dungeon"]  { --accent: var(--mk-dungeon); }
.marker[data-type="sacred"]   { --accent: var(--mk-sacred); }
.marker[data-type="sky"]      { --accent: var(--mk-sky); }

/* Settlements sit a little larger than wilds — hierarchy by size as well as by
   colour. Resting size and interaction response are separate factors and get
   multiplied, so hovering a city doesn't shrink it back down to the hover size
   of a wild. Scaling the glyph only; the hit disc is a fixed --hit and stays
   exactly where the spacing proof put it. */
.marker { --gbase: 1; --gmul: 1; }
.marker[data-type="city"] { --gbase: 1.16; }
.marker[data-type="fortress"],
.marker[data-type="sacred"] { --gbase: 1.06; }

/* Sits beside the glyph, out of flow. Flipping to the left is how colliding
   names are resolved at low zoom — see layoutLabels() in app.js. */
/* Warm and bookish with a heavy shadow, no plate. The map is mid-toned almost
   everywhere, so this reads over ochre, over green and over open water — and
   a plate would put 38 opaque rectangles over artwork we just spent the
   effort texturing. */
.marker-label {
  position: absolute;
  left: 0;
  top: 50%;
  /* Placed entirely by transform (app.js writes --lx/--ldy) so that when
     collision avoidance moves a name it can be transitioned. */
  translate: var(--lx, 20px) calc(-50% + var(--ldy, 0px));
  pointer-events: none;
  font-family: var(--label-serif);
  font-size: calc(16px * var(--lscale, 1));
  font-weight: 600;
  letter-spacing: 0.012em;
  line-height: 1;
  white-space: nowrap;
  color: var(--mk-label);
  text-shadow: var(--mk-label-shadow);
  opacity: var(--lbl);
  transition-property: opacity, color, translate;
  transition-duration: 130ms, 130ms, 220ms;
  transition-timing-function: var(--ease);
}

/* Progressive disclosure. Labelling all 38 at once is unreadable, so resting
   labels arrive in tiers as you zoom: settlements first, then landmarks, then
   everything. app.js writes data-labels on the viewport.

   Driven through a custom property rather than `opacity` directly, so the
   hover/active rules below only have to beat the low-specificity base
   declaration instead of the tier selectors. */
.marker { --lbl: 0; }

.viewport[data-labels="1"] .marker[data-type="city"],
.viewport[data-labels="2"] .marker[data-type="city"],
.viewport[data-labels="2"] .marker[data-type="fortress"],
.viewport[data-labels="2"] .marker[data-type="sacred"],
.viewport[data-labels="2"] .marker[data-type="sky"],
.viewport[data-labels="3"] .marker { --lbl: 1; }

/* Pointing at a place always names it, at any zoom. */
.marker:hover, .marker:focus-visible, .marker.is-active { --lbl: 1; }

.marker:hover, .marker:focus-visible { --gmul: 1.16; }

/* Selection fills the glyph solid in the seal colour — the outline/fill swap
   the glyph defs were built for, now reading as a colour change too. */
.marker.is-active { --accent: var(--seal); --gmul: 1.2; }
.marker.is-active .marker-glyph svg {
  filter: drop-shadow(0 0 3px var(--seal-ring-strong))
          drop-shadow(0 1px 1.5px rgba(20, 14, 6, 0.55));
}

/* Last, so a press still reads on an already-selected marker. */
.marker:active { --gmul: 0.96; }

.marker.is-hidden { opacity: 0; pointer-events: none; }
.marker.is-dimmed { opacity: 0.2; filter: saturate(0.2); pointer-events: none; }


/* ------------------------------------------------------------ detail panel */

.panel {
  position: absolute;
  z-index: 35;
  top: 14px;
  right: 14px;
  bottom: 14px;
  width: 424px;
  display: flex;
  flex-direction: column;
  border-radius: 22px;                 /* 10 + 12 padding — concentric */
  background: var(--surface);
  box-shadow: var(--shadow-panel);
  overflow: hidden;
  transition-property: opacity, translate, filter;
  transition-duration: 150ms;          /* exits are shorter than enters */
  transition-timing-function: var(--ease);
}

.panel[hidden] { display: none; }

.panel.is-leaving {
  opacity: 0;
  translate: 0 -12px;                  /* small fixed offset, not full height */
  filter: blur(4px);
  pointer-events: none;
}

.panel-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 12px;
}

.panel-close {
  position: absolute;
  top: 14px;
  right: 14px;
  z-index: 2;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  color: var(--ink-soft);
  background: var(--tint-06);
  transition-property: background-color, color, scale;
  transition-duration: 130ms;
  transition-timing-function: var(--ease);
}

.panel-close:hover { background: var(--tint-12); color: var(--ink); }
.panel-close:active { scale: 0.96; }
.panel-close svg { width: 16px; height: 16px; }

.panel-head { padding: 18px 52px 16px 18px; }

.panel-kicker {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 12px;
}

.panel-kicker .dot {
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.55;
}

.panel h2 {
  font-size: 30px;
  line-height: 1.12;
  letter-spacing: -0.012em;
}

.panel .subtitle {
  margin-top: 7px;
  font-size: 14.5px;
  font-style: italic;
  color: var(--ink-soft);
}

/* A view of the place. Only some entries carry one — see data/locations.js.
   The frame is 22 (panel) − 12 (scroll padding) = 10, so it stays concentric
   with the card it sits in. The aspect ratio is fixed and the image cropped to
   it, so an image of any shape can be dropped in without shifting the layout,
   and the panel doesn't reflow when a lazy image finally arrives. */
.photo {
  margin: 0 0 16px;                     /* matches panel-head's bottom padding */
}

.photo-frame {
  aspect-ratio: 1000 / 460;
  border-radius: 10px;
  overflow: hidden;
  background: var(--tint-10);           /* holds the space before the image lands */
  box-shadow: var(--shadow-border);
}

.photo-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: brightness(var(--photo-dim)) saturate(var(--photo-sat));
  opacity: 0;
  transform: scale(1.02);
  transition-property: opacity, transform;
  transition-duration: 420ms;
  transition-timing-function: var(--ease);
}

.photo.is-loaded .photo-frame img { opacity: 1; transform: scale(1); }

/* A file that isn't there shouldn't leave a grey hole in the panel. */
.photo.is-missing { display: none; }

.photo figcaption {
  padding: 8px 4px 0;
  font-size: 12px;
  line-height: 1.45;
  font-style: italic;
  color: var(--ink-soft);
}

.photo-credit {
  display: block;
  margin-top: 3px;
  font-style: normal;
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.panel-body {
  padding: 0 18px 18px;
  border-radius: 10px;
}

.panel-body p {
  font-size: 15.5px;
  line-height: 1.62;
  color: var(--ink-body);
}

.panel-body p + p { margin-top: 13px; }

.rule {
  height: 1px;                          /* structural divider — stays a border */
  margin: 20px 18px;
  background: linear-gradient(90deg, var(--rule-a), var(--rule-b) 62%, transparent);
}

.etym { padding: 0 18px 22px; }

.etym h3 {
  font-size: 10.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: 11px;
}

.etym p {
  font-size: 15px;
  line-height: 1.66;
  color: var(--ink-body-2);
}

.etym em { font-style: italic; }
.etym strong, .panel-body strong { font-weight: 600; }

/* Staged entrance — infrequent, so a stagger earns its keep here. */
.panel .stagger {
  opacity: 0;
  transform: translateY(10px);
  filter: blur(4px);
  animation: rise 380ms var(--ease) forwards;
}
/* Five steps because a place with an image has one more block than one without.
   Without the fifth rule the etymology would default to 0ms and arrive first. */
.panel .stagger:nth-child(1) { animation-delay: 0ms; }
.panel .stagger:nth-child(2) { animation-delay: 80ms; }
.panel .stagger:nth-child(3) { animation-delay: 160ms; }
.panel .stagger:nth-child(4) { animation-delay: 240ms; }
.panel .stagger:nth-child(5) { animation-delay: 320ms; }

@keyframes rise {
  to { opacity: 1; transform: translateY(0); filter: blur(0); }
}

/* ----------------------------------------------------------- search results */

.results {
  position: absolute;
  z-index: 38;
  top: 14px;
  right: 14px;
  width: 424px;
  max-height: calc(100% - 28px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 8px;
  border-radius: 18px;                 /* 10 + 8 padding — concentric */
  background: var(--surface);
  box-shadow: var(--shadow-panel);
}

.results[hidden] { display: none; }

/* -------------------------------------------------------- boot failure ----
   Shown only when the map data never arrived, in place of the blank viewport
   that used to be the sole symptom. It has to stand on its own: by definition
   nothing else on the page has been built when this appears. */

.boot-error {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(340px, calc(100% - 48px));
  padding: 22px 24px;
  border-radius: 18px;
  background: var(--surface);
  box-shadow: var(--shadow-panel);
  text-align: center;
  color: var(--ink-soft);
  font-size: 14px;
  line-height: 1.55;
}

.boot-error p { margin: 0 0 12px; }
.boot-error p:last-child { margin-bottom: 0; }
.boot-error strong { color: var(--ink); font-weight: 600; }

/* Not .btn: that one is drawn to sit on the dark topbar, and on this pale card
   it comes out as pale text on pale ground — all but invisible. */
.boot-retry {
  display: inline-flex;
  align-items: center;
  min-height: 34px;
  padding: 0 18px;
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--on-seal);
  background: var(--seal);
  border: 0;
  border-radius: 9px;
  cursor: pointer;
  transition: scale 140ms var(--ease);
}

.boot-retry:active { scale: 0.96; }

/* Nothing else got built, so the empty legend and the pan-and-zoom hint are
   both lies. Hide them rather than framing a failure with instructions. */
.viewport[data-boot-error] .legend,
.viewport[data-boot-error] .hint { display: none; }

/* The gazetteer still works without its artwork — markers, search and the
   place panels are all independent of it — so a failed map leaves the surround
   showing rather than taking the whole page down with it. */
.viewport[data-artless] #map-art { display: none; }

.results-empty {
  padding: 18px 12px;
  font-size: 14px;
  font-style: italic;
  color: var(--ink-soft);
}

.result {
  display: flex;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  min-height: 40px;
  padding: 9px 11px;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 10px;
  cursor: pointer;
  transition-property: background-color;
  transition-duration: 110ms;
  transition-timing-function: var(--ease);
}

.result:hover { background: var(--tint-07); }
.result .r-name { font-size: 15px; }
.result .r-meta {
  margin-left: auto;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* -------------------------------------------------------------------- hint */

.hint {
  position: absolute;
  z-index: 20;
  left: 50%;
  bottom: 18px;
  translate: -50% 0;
  padding: 8px 15px;
  border-radius: 999px;
  font-size: 12.5px;
  letter-spacing: 0.04em;
  color: rgba(233, 220, 186, 0.82);
  background: rgba(20, 17, 12, 0.78);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 0 0 1px rgba(233, 220, 186, 0.12);
  pointer-events: none;
  transition-property: opacity, translate;
  transition-duration: 400ms;
  transition-timing-function: var(--ease);
}

.hint.is-gone { opacity: 0; translate: -50% 8px; }

/* ------------------------------------------------------------------ mobile */

@media (max-width: 820px) {
  .brand .tagline { display: none; }
  .search input { width: 130px; }
  .search input:focus { width: 168px; }

  .panel, .results {
    top: auto;
    left: 10px;
    right: 10px;
    bottom: 10px;
    width: auto;
    max-height: 74%;
    border-radius: 20px;
  }

  .panel.is-leaving { translate: 0 12px; }

  /* Shallower crop on the bottom sheet — the picture shouldn't push the first
     line of text off a 74%-height panel. 20 − 12 padding keeps it concentric. */
  .photo-frame { aspect-ratio: 1000 / 380; border-radius: 8px; }

  .legend {
    left: 10px;
    right: 10px;
    bottom: auto;
    top: 10px;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    padding: 7px;
    border-radius: 15px;
  }

  .legend-title { display: none; }
  .legend .count { display: none; }
  .legend button { min-height: 32px; font-size: 12px; }

  .hint { bottom: 12px; font-size: 11.5px; }
}

@media (max-width: 560px) {
  .topbar { gap: 10px; padding: 10px 12px; }
  .brand h1 { font-size: 15px; letter-spacing: 0.22em; text-indent: 0.22em; }
  .zoom-readout { display: none; }
  .panel h2 { font-size: 25px; }
}

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

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .panel .stagger { opacity: 1; transform: none; filter: none; }
  .panel.is-leaving { translate: 0; filter: none; }
  /* Kill the hover/press response, keep the per-kind resting size. */
  .marker:hover, .marker:focus-visible, .marker:active { --gmul: 1; }
  .icon-swap .i { filter: none !important; }
}
