:root {
  --navy: #1f2d3d;
  --green: #2f6f4f;
  --paper-bg: #ffffff;
  --panel-bg: #f4f6f5;
  --border: #d4dad7;
  --text-muted: #6b7a76;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--panel-bg);
  color: var(--navy);
}

body {
  display: flex;
  flex-direction: column;
}

.app {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* Floats over the map rather than occupying its own row in the layout —
   that used to push the map down and leave a bare strip above it whenever
   the banner was hidden or its height changed. */
.announcement-banner {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 1500;
  max-width: calc(100% - 48px);
  background: #ffe9a8;
  color: #4a3b00;
  padding: 8px 16px;
  border-radius: 6px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.25);
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  font-size: 0.85rem;
  text-align: center;
}
.announcement-banner[hidden] { display: none; }

#announcement-dismiss {
  border: none;
  background: none;
  color: inherit;
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}

/* ---------------- Save toast ---------------- */
/* Transient confirmation ("Map saved to ...") — mirrors the .modal-overlay/
   .open opacity-transition pattern rather than the [hidden] attribute, since
   its visibility needs to animate in/out on a timer, not just toggle. */

.toast {
  position: fixed;
  left: 50%;
  bottom: 28px;
  transform: translate(-50%, 8px);
  background: var(--navy);
  color: #fff;
  padding: 10px 18px;
  border-radius: 6px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  font-size: 0.88rem;
  font-weight: 600;
  z-index: 3000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.toast.show {
  opacity: 1;
  transform: translate(-50%, 0);
}

.app-header {
  flex: 0 0 auto;
  background: var(--navy);
  color: #fff;
  padding: 10px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}
.app-header-brand {
  display: flex;
  align-items: center;
  gap: 14px;
  min-width: 0;
}
/* The source artwork is a wide horizontal lockup (icon + wordmark) — fixing
   the height and letting width scale keeps it legible at any header size
   without distorting it. */
.app-logo {
  flex: 0 0 auto;
  height: 34px;
  width: auto;
}
.app-header h1 { margin: 0; font-size: 1.15rem; }
.app-header .tagline { margin: 1px 0 0; font-size: 0.8rem; color: #cfd8e3; }

.account-widget { flex: 0 0 auto; }
.account-signed-out,
.account-signed-in {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* Class selectors above tie the [hidden] UA style on specificity, and author
   rules win ties — so a plain `display: flex` would show these regardless of
   the `hidden` attribute JS toggles. This attribute-qualified rule outranks
   it and lets `hidden` actually hide the element. */
.account-signed-out[hidden],
.account-signed-in[hidden] {
  display: none;
}

.account-username {
  font-size: 0.85rem;
  font-weight: 600;
  color: #cfd8e3;
}

.account-btn {
  padding: 6px 12px;
  border: 1px solid rgba(255,255,255,0.5);
  background: transparent;
  color: #fff;
  border-radius: 5px;
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
}
.account-btn:hover { background: rgba(255,255,255,0.12); }
.account-btn-primary {
  background: #fff;
  color: var(--navy);
  border-color: #fff;
}
.account-btn-primary:hover { background: #e7ecea; }

.layout {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr 320px;
  grid-template-rows: 1fr;
  grid-template-areas: "front controls";
  gap: 0;
}

.front-composer {
  grid-area: front;
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

/* ---------------- Controls sidebar ---------------- */

.controls {
  grid-area: controls;
  background: #fff;
  border-left: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 8px 16px 16px;
  min-height: 0;
  overflow-y: auto;
}

/* The active tab gets the header's navy fill with white text so it reads as
   the current, selected tab with strong contrast; the inactive tab sits on
   the same white surface as the panel below it (background inherited from
   .controls). */
.sidebar-tabs {
  display: flex;
  gap: 0;
  margin: 8px 0 4px;
}
.sidebar-tab {
  flex: 1;
  padding: 10px 10px 9px;
  border: none;
  background: none;
  border-radius: 0;
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--navy);
}
.sidebar-tab:hover { background: var(--panel-bg); color: var(--navy); }
.sidebar-tab.active {
  background: var(--navy);
  font-weight: 700;
  color: #fff;
}
.sidebar-tab.active:hover { background: #14202e; color: #fff; }

.sidebar-pane { display: none; }
.sidebar-pane.active { display: block; }

fieldset {
  border: none;
  border-top: 1px solid var(--border);
  padding: 12px 0;
  margin: 0;
  min-width: 0; /* fieldsets default to min-content width, ignoring the container — this lets long group/map names actually truncate instead of overflowing the sidebar */
}
fieldset:first-of-type { border-top: none; }

legend {
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--green);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 0;
  margin-bottom: 6px;
}

label {
  display: block;
  font-size: 0.82rem;
  font-weight: 600;
  margin: 10px 0 4px;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
select,
textarea {
  width: 100%;
  padding: 7px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  font-size: 0.9rem;
  font-family: inherit;
  background: #fff;
  color: var(--navy);
}

textarea { resize: vertical; }

.hint {
  font-size: 0.75rem;
  color: var(--text-muted);
  margin: 8px 0 0;
}

/* Custom stand-in for a native <input list>/<datalist> dropdown, which
   browsers render themselves with no styling hook — so its width and text
   alignment can't be made to match the rest of the app. This one is a plain
   positioned list, sized and aligned to match the input above it. */
.group-input-wrap { position: relative; margin-bottom: 10px; }
.group-suggestions {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  width: 100%;
  max-height: 180px;
  overflow-y: auto;
  margin: 0;
  padding: 4px 0;
  list-style: none;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 5px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.15);
  z-index: 20;
}
.group-suggestion-item {
  padding: 6px 10px;
  font-size: 0.9rem;
  color: var(--navy);
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.group-suggestion-item:hover,
.group-suggestion-item.active {
  background: var(--panel-bg);
  color: var(--green);
}

/* Styled to match the app's primary/secondary buttons (e.g. "Preview front
   & back pages") — the native radio input is kept (for existing state/
   behavior) but visually hidden, with the surrounding label showing
   checked/focus state via :has(). Unchecked looks like .secondary-btn;
   checked looks like .primary-btn. */
.radio-row {
  display: flex;
  gap: 8px;
  margin-top: 4px;
}
.radio-btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 10px 12px;
  margin: 0;
  border: 1px solid var(--navy);
  border-radius: 6px;
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--navy);
  background: #fff;
  cursor: pointer;
}
.radio-btn:hover { background: var(--panel-bg); }
.radio-btn:has(input:checked) {
  background: var(--navy);
  color: #fff;
}
.radio-btn:has(input:checked):hover { background: #14202e; }
.radio-btn:has(input:focus-visible) {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}
.radio-btn input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

.current-scale-box {
  padding: 9px 10px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--panel-bg);
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--navy);
  letter-spacing: 0.01em;
}

.scale-snap-row {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.scale-snap-btn {
  flex: 1;
  padding: 7px 6px;
  border: 1px solid var(--green);
  background: #fff;
  color: var(--green);
  border-radius: 5px;
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 600;
}
.scale-snap-btn:hover { background: var(--green); color: #fff; }

.preset-btn-row {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}
.preset-btn-row .secondary-btn {
  width: auto;
  flex: 1;
  margin-bottom: 0;
}

.group-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
/* A flex/grid display on the element itself always beats the UA [hidden]
   stylesheet rule at equal specificity (author origin wins ties) — without
   this, toggling `hidden` on a `.group-list` does nothing and it never
   actually collapses. */
.group-list[hidden] { display: none; }
.group-list li { margin: 0; }
.group-list-item {
  width: 100%;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 7px 8px;
  border: none;
  border-radius: 5px;
  background: none;
  color: var(--navy);
  font-size: 0.88rem;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
}
.group-list-item:hover { background: var(--panel-bg); }
/* The row's expand chevron (a separate button — see .group-expand-toggle
   below) is a later sibling of this button, so :has() reaches forward from
   the row to color the name once that chevron is in its expanded state. */
.group-list-item-row:has(.group-expand-toggle.expanded) .group-list-name { color: var(--green); }
.group-list-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.group-list-badge {
  flex: 0 0 auto;
  font-size: 0.65rem;
  font-weight: 700;
  color: var(--text-muted);
  background: var(--panel-bg);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  padding: 2px 6px;
  border-radius: 10px;
  margin-left: 4px;
}
.group-list-item-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.group-list-delete-btn {
  border: none;
  background: none;
  color: #b3261e;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
  flex: 0 0 auto;
}
.group-list-delete-btn:hover { background: #fdecea; border-radius: 4px; }

.group-list-print-btn {
  border: none;
  background: none;
  color: var(--navy);
  line-height: 1;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 4px;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
}
.group-list-print-btn:hover { background: var(--panel-bg); color: var(--green); }

.group-color-swatch {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  cursor: pointer;
}
.group-color-swatch:disabled { cursor: default; opacity: 0.85; }

/* Popover of 16 preset colours shown when a group's colour swatch is
   clicked — replaces the browser's native <input type=color> picker so
   every group's colour comes from the same fixed, mutually-distinct set. */
.group-color-popover {
  position: absolute;
  z-index: 1600;
  display: grid;
  grid-template-columns: repeat(4, 26px);
  gap: 6px;
  padding: 8px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
}
.group-color-option {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 2px solid #fff;
  outline: 1px solid var(--border);
  border-radius: 5px;
  cursor: pointer;
}
.group-color-option:hover { outline-color: var(--navy); }
.group-color-option.selected { outline: 2px solid var(--navy); }

/* Shows/hides a group's maps as coloured outlines on the map — an eye icon
   that switches to a slashed eye, coloured red, once hidden. */
.group-visibility-toggle {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 5px;
  background: none;
  color: var(--navy);
  cursor: pointer;
}
.group-visibility-toggle:hover { background: var(--panel-bg); }
.group-visibility-toggle.hidden-mode { color: #b3261e; }
.group-visibility-toggle.hidden-mode:hover { background: #fdecea; }

/* Expand/collapse toggle for a group's inline map list — a dedicated,
   generously-sized button at the far right of the row (the name/badge
   button earlier in the row also toggles, so most of the row is clickable;
   this is just the deliberately large, easy-to-hit dedicated control). */
.group-expand-toggle {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 5px;
  background: none;
  color: var(--navy);
  cursor: pointer;
}
.group-expand-toggle:hover { background: var(--panel-bg); color: var(--green); }
.group-list-chevron {
  display: block;
  transform: rotate(-90deg);
  transition: transform 0.12s ease;
}
.group-expand-toggle.expanded { color: var(--green); }
.group-expand-toggle.expanded .group-list-chevron { transform: rotate(0deg); }

/* A group's maps, shown inline beneath it (an accordion) instead of
   navigating to a separate page — indentation alone (no vertical rule or
   card borders) signals the nesting, like a plain file/nav tree. */
.group-list-nested {
  margin: 0 0 2px;
  padding-left: 20px;
}
.group-list-nested .group-list-item {
  font-weight: 500;
  font-size: 0.85rem;
}

.primary-btn {
  width: 100%;
  padding: 10px 12px;
  background: var(--navy);
  color: #fff;
  border: none;
  border-radius: 6px;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
}
.primary-btn:hover { background: #14202e; }

.secondary-btn {
  width: 100%;
  padding: 10px 12px;
  background: #fff;
  color: var(--navy);
  border: 1px solid var(--navy);
  border-radius: 6px;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  margin-bottom: 8px;
}
.secondary-btn:hover { background: var(--panel-bg); }

.danger-btn {
  width: 100%;
  padding: 10px 12px;
  background: #fff;
  color: #b3261e;
  border: 1px solid #b3261e;
  border-radius: 6px;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  margin-bottom: 8px;
}
.danger-btn:hover { background: #fdecea; }

/* ---------------- Preview area ---------------- */

.column-heading {
  font-size: 0.95rem;
  color: var(--text-muted);
  margin: 0 0 10px;
  font-weight: 600;
}

.page-wrapper {
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0,0,0,0.15);
  border-radius: 2px;
}

.page {
  background: var(--paper-bg);
  transform-origin: top left;
  display: flex;
  flex-direction: column;
  /* Narrow margin so the front page's printable map area fills as much of
     the sheet as possible — the back page overrides this to 0 (full-bleed
     legend image) below, so this only affects the front page in practice. */
  padding: 5mm;
}

/* Paper sizes (screen + print share these dimensions) */
.page.size-a4.orient-portrait  { width: 210mm; height: 297mm; }
.page.size-a4.orient-landscape { width: 297mm; height: 210mm; }
.page.size-a3.orient-portrait  { width: 297mm; height: 420mm; }
.page.size-a3.orient-landscape { width: 420mm; height: 297mm; }

/* ---- Front page ---- */
/* The front page shows only the map — title, scale, date and attribution all
   live on the back page instead. */

.map-frame {
  position: relative;
  flex: 1 1 auto;
  overflow: hidden;
  min-height: 0;
}
#map { width: 100%; height: 100%; background: #d2e7f4; }

.north-arrow {
  position: absolute;
  top: 8px;
  right: 8px;
  height: 15px;
  display: flex;
  align-items: center;
  /* Opaque, not rgba(...,0.85) — see GRID_LINE_COLOR in script.js. Anything
     partly transparent on the printable page forces the printer to flatten
     the whole sheet's transparency, which visibly degrades the map raster
     underneath. This badge sits over the map, so it counts. */
  background: #fff;
  border-radius: 4px;
  padding: 0 6px;
  z-index: 1100;
  pointer-events: none;
}
.north-arrow svg {
  height: 100%;
  width: auto;
  display: block;
}

/* NZTM grid reference labels — shown only in the front-page preview and print
   (see #preview-front-pane wiring and the print media block); hidden by
   default via the [hidden] attribute, toggled from script.js. */
.grid-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1000;
}
.grid-label {
  position: absolute;
  font-weight: 700;
  font-size: 11px;
  color: #1a1a1a;
  font-family: inherit;
  white-space: nowrap;
  text-shadow:
    -1px -1px 0 #fff, 1px -1px 0 #fff,
    -1px 1px 0 #fff, 1px 1px 0 #fff;
}

/* ---- Full-screen map chooser (screen only) ---- */
/* On screen, the front page fills the composer edge-to-edge and the map is
   freely interactive; a masked "hole" shows the area that will print. The
   page-shaped mockup (a full-bleed map with a small margin) is reserved for print. */

@media screen {
  .front-composer #front-wrapper {
    width: 100%;
    height: 100%;
    box-shadow: none;
    border-radius: 0;
  }
  .front-composer #front-page {
    width: 100%;
    height: 100%;
    padding: 0;
  }
}

/* When previewing, the front page is temporarily moved into the modal and
   shown as a plain paper mockup — no live-chooser mask/handles/zoom control. */
#preview-front-pane .mask-hole,
#preview-front-pane .frame-handle {
  display: none;
}
#preview-front-pane .map-frame {
  border: 1px solid var(--navy);
}
#preview-front-pane .leaflet-control-zoom,
#preview-front-pane .leaflet-control-attribution {
  display: none;
}

.mask-hole {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  box-shadow: 0 0 0 2000px rgba(35, 40, 35, 0.55);
  border: 2px solid #fff;
  pointer-events: none;
  /* #map has position:relative but no z-index, so it never establishes its own
     stacking context — Leaflet's internal panes (tile pane is z-index:200) would
     otherwise leak out and paint above this mask despite coming first in the DOM.
     Kept below 1000 so it doesn't cover Leaflet's own zoom control. */
  z-index: 900;
}

.frame-handle {
  position: absolute;
  width: 11px;
  height: 11px;
  background: #fff;
  border: 1px solid var(--navy);
}
.frame-handle.tl { top: -6px; left: -6px; }
.frame-handle.tr { top: -6px; right: -6px; }
.frame-handle.bl { bottom: -6px; left: -6px; }
.frame-handle.br { bottom: -6px; right: -6px; }

/* Directory tab: browse the map without the print-area mask/frame. */
body.directory-mode .mask-hole {
  display: none;
}

/* ---- Back page: full-page legend image with dynamic overlays ---- */

.back-page {
  padding: 0;
  overflow: hidden;
  position: relative;
}

.legend-image-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
}

/* The legend image/overlays are authored for a portrait page. On a landscape
   page they'd otherwise letterbox down to a small centered strip, so instead
   the whole layout is built at the page's portrait-equivalent size and
   rotated 90° left (counter-clockwise) to fill the landscape sheet edge to
   edge — script.js's positionLegendOverlays() mirrors this by computing
   overlay positions against the swapped (portrait) dimensions too. */
.back-page.orient-landscape .legend-image-wrap {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(-90deg);
}
.back-page.size-a4.orient-landscape .legend-image-wrap {
  width: 210mm;
  height: 297mm;
}
.back-page.size-a3.orient-landscape .legend-image-wrap {
  width: 297mm;
  height: 420mm;
}

.legend-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.legend-overlay {
  position: absolute;
  font-family: inherit;
  color: #1a1a1a;
  white-space: nowrap;
}

.legend-title-overlay {
  left: 56.5%;
  top: 90.86%;
  transform: translateY(-50%);
  font-size: 3.9mm;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
}

.legend-date-overlay {
  left: 59.63%;
  top: 93.06%;
  transform: translateY(-50%);
  font-size: 3.9mm;
  font-weight: 600;
}

/* "Date printed" — see script.js's refreshPrintedOverlay(). The label
   itself ("Date printed:") is baked into the legend artwork now, directly
   below "Date created:", so this is value-only, matching the other fields
   whose labels are also part of the image. */
.legend-printed-overlay {
  left: 59.2%;
  top: 94.87%;
  transform: translateY(-50%);
  font-size: 3.9mm;
  font-weight: 600;
}

.legend-mapref-overlay {
  left: 70.05%;
  top: 84.4%;
  font-size: 3.9mm;
  line-height: 1.05;
  font-weight: 600;
  white-space: nowrap;
}

.legend-declination-overlay {
  left: 90.98%;
  top: 73.06%;
  transform: translateY(-50%);
  font-size: 3.9mm;
  font-weight: 600;
}

/* Only shown for 1:100,000-and-coarser maps (see updateDeclination()) — a
   single centre reading is less representative once the sheet covers enough
   ground for declination to meaningfully differ across it. Sits in the gap
   between the centre reading (right after "Declination:") and "Scale:"
   below; smaller than the main reading since it's supplementary. */
.legend-declination-corners-overlay {
  left: 85.02%;
  top: 76.4%;
  transform: translateX(-50%);
  font-size: 3.2mm;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  white-space: nowrap;
}

/* "Scale" — label baked into the legend artwork; see script.js's
   updateLiveScale(). */
.legend-scale-overlay {
  left: 81.66%;
  top: 83.32%;
  transform: translateY(-50%);
  font-size: 3.9mm;
  font-weight: 600;
}

/* "Group Name/Colour" — label baked into the legend artwork; only shown
   when printed from the Library (see script.js's print button). */
.legend-group-overlay {
  left: 26.08%;
  top: 97.86%;
  transform: translateY(-50%);
  font-size: 3.9mm;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 1.5mm;
}
.legend-group-overlay[hidden] {
  display: none;
}

.legend-group-swatch {
  display: inline-block;
  width: 3.5mm;
  height: 3.5mm;
  border-radius: 1px;
  border: 0.3mm solid rgba(0, 0, 0, 0.25);
}

/* Moved into the blank strip between "Alpine features" and "Group
   Name/Colour:" — the new artwork's own bottom-left corner is now taken up
   by that field, leaving no room for this at its old position. */
.legend-attribution-overlay {
  left: 2.6%;
  top: 96.61%;
  font-size: 2mm;
  color: #6b7a76;
}


/* Leaflet scale control tweak to match sheet style, and shifted left
   so it sits beside the north arrow instead of overlapping it. */
.leaflet-control-scale-line {
  /* Opaque for the same reason as .north-arrow above — this prints too. */
  background: #fff;
  font-size: 9px;
}
.leaflet-top.leaflet-right .leaflet-control-scale {
  margin-top: 8px;
  margin-right: 44px;
}

/* ---------------- Preview modal ---------------- */

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(20, 26, 32, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}
.modal-overlay.open {
  opacity: 1;
  pointer-events: auto;
}

.modal-box {
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
  width: min(720px, 92vw);
  height: min(860px, 88vh);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.modal-header {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 10px 10px 14px;
  border-bottom: 1px solid var(--border);
}

/* ---------------- Preview modal: page preview + details/print/save sidebar ---------------- */

.preview-modal-box {
  width: min(1040px, 95vw);
}

.preview-modal-content {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  overflow: hidden;
}

.preview-sidebar {
  flex: 0 0 300px;
  border-left: 1px solid var(--border);
  padding: 8px 16px 16px;
  overflow-y: auto;
  background: #fff;
}
.preview-sidebar fieldset:first-of-type { border-top: none; }

.modal-tabs { display: flex; gap: 6px; }
.modal-tab {
  padding: 6px 14px;
  border: 1px solid var(--border);
  background: #fff;
  border-radius: 5px;
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text-muted);
}
.modal-tab.active {
  background: var(--navy);
  border-color: var(--navy);
  color: #fff;
}

.modal-close {
  border: none;
  background: none;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  color: var(--text-muted);
  padding: 2px 8px;
}
.modal-close:hover { color: var(--navy); }

.modal-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: var(--panel-bg);
  overflow: auto;
}

.modal-pane { display: none; }
.modal-pane.active {
  display: flex;
  width: 100%;
  height: 100%;
  align-items: center;
  justify-content: center;
  overflow: auto;
}

/* ---------------- Auth modal ---------------- */

.auth-modal-box {
  width: min(380px, 90vw);
  height: auto;
}

.auth-modal-body {
  display: block;
  padding: 28px;
}

/* Consistent gap between fields instead of stacked label/input margins —
   keeps each field's own label snug while giving real breathing room
   between one field and the next. */
.auth-pane {
  display: none;
  flex-direction: column;
  gap: 16px;
}
.auth-pane.active { display: flex; }
.auth-pane label { margin: 0 0 2px; }
.auth-pane .primary-btn { margin-top: 8px; }
.auth-pane .hint { margin-top: -8px; } /* sits with the field above it, not the gap below */
.auth-pane .auth-error,
.auth-pane .auth-success { margin-top: 0; }

.auth-error {
  font-size: 0.8rem;
  color: #b3261e;
  margin: 10px 0 0;
}

.auth-success {
  font-size: 0.8rem;
  color: var(--green);
  margin: 10px 0 0;
}

/* ---------------- Account modal ---------------- */

.account-modal-box {
  width: min(520px, 90vw);
  height: auto;
  max-height: 80vh;
}

.account-modal-body {
  display: block;
  padding: 24px;
  overflow-y: auto;
}

.account-pane { display: none; }
.account-pane.active { display: block; }

.account-pane p { margin: 0 0 4px; }

#edit-profile-form .secondary-btn,
#change-password-form .secondary-btn { margin-top: 8px; }

/* A section per concern (profile, password, danger zone) rather than a
   single unbroken form — a divider plus consistent vertical rhythm reads
   as distinct settings groups instead of one long list of fields. */
.account-section {
  padding: 18px 0;
  border-top: 1px solid var(--border);
}
.account-section:first-child { padding-top: 0; border-top: none; }
.account-section h3 {
  margin: 0 0 12px;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--navy);
}

.account-danger-zone {
  margin-top: 4px;
  padding: 16px;
  border: 1px solid #f0c4bf;
  border-top: 1px solid #f0c4bf;
  border-radius: 8px;
  background: #fdf4f3;
}
.account-danger-zone h3 { color: #b3261e; }

.account-profile-header {
  display: flex;
  align-items: center;
  gap: 14px;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}
.account-avatar {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--navy);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.account-profile-text { min-width: 0; }
.account-profile-name {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--navy);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-profile-email {
  margin: 2px 0 0;
  font-size: 0.85rem;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-maps-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}
.account-maps-list li {
  padding: 8px 4px;
  border-bottom: 1px solid var(--border);
  font-size: 0.88rem;
  color: var(--navy);
}
.account-maps-list li:last-child { border-bottom: none; }
.account-maps-list li.account-map-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.account-map-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-maps-list li.account-map-row-nested {
  padding-left: 20px;
}
.account-maps-list li.account-map-group-header {
  padding-top: 14px;
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}
.account-maps-list li.account-map-group-header:first-child { padding-top: 8px; }

/* ---------------- Admin modal ---------------- */

.admin-modal-box {
  width: min(960px, 95vw);
  height: min(600px, 85vh);
}

.admin-modal-body {
  display: block;
  padding: 20px;
  overflow: auto;
}

.admin-pane { display: none; }
.admin-pane.active { display: block; }

.settings-credits-list {
  margin: 8px 0 16px;
  padding: 0;
  list-style: none;
  font-size: 0.85rem;
}
.settings-credits-list li {
  padding: 6px 0;
  border-bottom: 1px solid var(--border, #e2e2e2);
}
.settings-credits-list li:last-child { border-bottom: none; }

.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}
.admin-table th,
.admin-table td {
  text-align: left;
  padding: 6px 8px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.admin-table th { color: var(--text-muted); font-weight: 600; }

.admin-table-btn {
  padding: 4px 8px;
  border: 1px solid var(--border);
  background: #fff;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.78rem;
  font-weight: 600;
  margin-right: 4px;
}
.admin-table-btn:hover { background: var(--panel-bg); }
.admin-table-btn.danger { border-color: #b3261e; color: #b3261e; }
.admin-table-btn.danger:hover { background: #fdecea; }

/* ---------------- Admin Email update tab ---------------- */

.admin-email-recipient-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  max-height: 180px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
}
.admin-email-recipient-list li {
  padding: 6px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem;
}
.admin-email-recipient-list li:last-child { border-bottom: none; }
.admin-email-recipient-list label {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-weight: 400;
  cursor: pointer;
}

/* ---------------- Admin Labels tab: maps sub-items ---------------- */

.admin-label-maps-row td {
  background: var(--panel-bg);
  padding: 10px 8px;
  white-space: normal;
}

.admin-label-maps-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.admin-label-maps-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.admin-label-maps-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: 0.85rem;
}
.admin-label-maps-empty {
  color: var(--text-muted);
  font-style: italic;
  font-size: 0.85rem;
}

.admin-label-maps-add-row {
  display: flex;
  gap: 8px;
  align-items: center;
}
.admin-label-maps-add-row select {
  flex: 1 1 auto;
  padding: 5px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 0.82rem;
  background: #fff;
  color: var(--navy);
}

.admin-label-managers-panel {
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.admin-label-managers-heading {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.02em;
}

/* ---------------- Responsive (tablet / phone) ---------------- */
/* Below this width there isn't room for the fixed 320px sidebar beside the
   map chooser (see .layout's grid-template-columns above) — stack the map
   on top and the sidebar below instead, each scrolling independently.
   Untouched above this width; the desktop two-column layout is unaffected. */
@media screen and (max-width: 900px) {
  .layout {
    grid-template-columns: 1fr;
    grid-template-rows: minmax(280px, 50vh) 1fr;
    grid-template-rows: minmax(280px, 50dvh) 1fr;
    grid-template-areas: "front" "controls";
  }
  .controls {
    border-left: none;
    border-top: 1px solid var(--border);
  }

  .app-header { flex-wrap: wrap; row-gap: 8px; }

  /* The preview modal's page mockup and its details/print/save panel sit
     side by side on desktop (see .preview-modal-content/.preview-sidebar
     above) — there's no room for a fixed 300px side panel once the modal
     itself is only as wide as the screen, so stack them instead. */
  .preview-modal-content {
    flex-direction: column;
    overflow-y: auto;
  }
  /* Without a fixed share of the height, flexbox would give this only
     whatever's left over after the (fairly tall) form below it claims space
     first — squeezing the page mockup down to almost nothing. Reserving a
     set share up front, and letting the column as a whole scroll instead,
     keeps the page itself always visibly readable. Scoped to #preview-modal
     specifically — .modal-body is also reused (combined with a second class)
     as the plain scrollable body of the auth/account/admin modals, which
     have no page-mockup/sidebar split and must keep growing to fill their box. */
  #preview-modal .modal-body {
    flex: 0 0 auto;
    height: 42vh;
    height: 42dvh;
    min-height: 220px;
  }
  .preview-sidebar {
    flex: 0 0 auto;
    width: 100%;
    border-left: none;
    border-top: 1px solid var(--border);
    overflow-y: visible;
  }

  /* Bumps a handful of icon-only buttons in the Library list a bit closer to
     a comfortable touch-target size — harmless at their old size on desktop,
     but easy to miss-tap on a phone. */
  .group-visibility-toggle,
  .group-color-swatch { width: 36px; height: 36px; }
  .group-expand-toggle { width: 40px; height: 40px; }
  .group-list-print-btn,
  .group-list-delete-btn { padding: 8px; }
}

@media screen and (max-width: 520px) {
  /* Every modal shares .modal-box; giving it the full screen here (rather
     than its usual centered, capped-size card) means none of the four ever
     has less room than the phone's own viewport. Each modal's own width/
     height rule below (declared later in this file than .modal-box, so it
     wins at equal specificity) then fits its actual content into that. */
  .modal-box { width: 100vw; border-radius: 0; }
  .preview-modal-box,
  .admin-modal-box {
    height: 100vh;
    height: 100dvh;
    max-height: 100vh;
    max-height: 100dvh;
  }
  .auth-modal-box,
  .account-modal-box {
    max-height: 100vh;
    max-height: 100dvh;
  }
  .auth-modal-body,
  .account-modal-body,
  .admin-modal-body {
    padding: 16px;
  }
  .app-header { padding: 10px 14px; }
  .app-header h1 { font-size: 1rem; }

  /* Four admin tabs (or two preview/auth/account tabs) can be wider than a
     narrow phone screen — scrolling the tab row instead of letting it wrap
     or overflow keeps the close button in a fixed, reachable spot. */
  .modal-tabs { overflow-x: auto; }
}

/* ---------------- Print ---------------- */

/* Covers the true-size page (see body.print-sizing below) for as long as
   printCurrentMap() is still waiting on tiles, so whatever the user was
   actually looking at (the Library list, or the preview modal) reads as
   undisturbed for the whole wait instead of jumping straight to a loose
   full-size page. An earlier version of this fix instead parked the
   true-size page off-screen (position:fixed; left:-100000px) to hide it —
   but Chrome deprioritizes rendering quality for content it considers
   off-screen (lower-resolution/less current tiles), so that traded the
   visible flash for washed-out colour and blurry tiles in the actual
   printed output. This plain opaque curtain sits directly on top of the
   page while it stays in its ordinary on-screen position — hiding the same
   flash without telling Chrome the content underneath isn't worth
   rendering at full quality. Removed the instant tiles are confirmed ready,
   immediately before window.print() fires. */
#print-prep-curtain {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100000;
  background: var(--paper-bg);
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  font-size: 1rem;
  font-weight: 600;
  color: var(--navy);
}
body.print-curtain #print-prep-curtain {
  display: flex;
}
.print-prep-spinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 3px solid var(--border);
  border-top-color: var(--green);
  animation: print-prep-spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .print-prep-spinner { animation: none; }
}
@keyframes print-prep-spin {
  to { transform: rotate(360deg); }
}
.print-prep-message {
  font-size: 1rem;
  font-weight: 600;
  color: var(--navy);
}
.print-prep-timer {
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.print-prep-progress {
  font-size: 0.85rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  min-height: 1.2em;
}
/* Sits under the "Current scale" box — see updatePrintDpiReadout(). */
.print-dpi-readout {
  margin: 6px 0 0;
  font-size: 0.85rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  min-height: 1.2em;
}
.print-dpi-readout.is-source-limited {
  color: var(--navy);
}
.print-prep-extended {
  margin-top: 8px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  max-width: 280px;
  text-align: center;
}
.print-prep-extended p {
  margin: 0;
  font-size: 0.92rem;
  font-weight: 500;
  color: var(--text-muted);
}
.print-prep-extended-actions {
  display: flex;
  gap: 10px;
}
.print-prep-extended-actions .primary-btn,
.print-prep-extended-actions .secondary-btn {
  width: auto;
  margin-bottom: 0;
}
/* body.print-sizing (below) hides every descendant of body except
   .printable-page — the curtain is a plain child of body too, so without
   this explicit exception it would get hidden right along with everything
   else it's meant to be covering, silently doing nothing for the whole
   wait. */
body.print-sizing #print-prep-curtain,
body.print-sizing #print-prep-curtain * {
  visibility: visible;
}

/* Forces the same true (unscaled) physical page geometry as @media print
   below, but while still on-screen — added just before printCurrentMap()'s
   tile pre-warm/wait in script.js (with body.print-curtain, above, covering
   it for as long as that wait takes), and removed again afterwards. Without
   this, Leaflet measures and loads tiles for whatever size the page/map
   happens to be on screen (the preview modal's shrunk-to-fit size, or the
   live composer's arbitrary window size) — and @media print's own resize to
   the true print dimensions only happens synchronously inside window.print()
   itself, too late for Leaflet to fetch the now-missing tiles before the
   browser captures the printed page. That's what made prints come out
   smaller/cropped and not match the preview: the map was still showing
   tiles sized for the wrong container. Pre-empting the resize here, before
   the wait, gives Leaflet time to load the right tiles first — so by the
   time @media print (or a bare Ctrl+P) actually applies, nothing changes
   size and there's nothing left to (re)load. */
body.print-sizing .app { display: block; height: auto; }
body.print-sizing .layout { padding: 0; display: block; height: auto; }

/* A closed modal-overlay is still display:flex/position:fixed the rest of
   the time (see .modal-overlay above), not display:none — so without this,
   every closed modal (auth, account, admin) would be forced into normal flow
   by the rule below like the rest of this block, and its real height
   (possibly-tall forms/tables, still present in the DOM even while closed)
   would add extra blank space after the printable pages — enough, in
   practice, to overflow onto its own extra blank page. #preview-modal is
   deliberately excluded: unlike the other three, it's not purely decorative
   when closed — #back-wrapper (the back/legend page) lives inside it
   permanently and never moves into .front-composer the way the front page
   does, so it must stay in flow and print even when the preview was never
   opened (printing straight from the Library). Higher specificity than the
   plain .modal-overlay rule below (two classes vs one), so this wins
   regardless of source order. */
body.print-sizing .modal-overlay:not(.open):not(#preview-modal) { display: none !important; }

body.print-sizing .front-composer,
body.print-sizing .modal-overlay,
body.print-sizing .modal-box,
body.print-sizing .preview-modal-content,
body.print-sizing .modal-body {
  display: block;
  position: static;
  width: auto;
  height: auto;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  background: none;
  border: none;
  box-shadow: none;
  opacity: 1;
  overflow: visible;
}
body.print-sizing .modal-pane { display: block !important; }
body.print-sizing .page-wrapper {
  width: auto !important;
  height: auto !important;
  overflow: visible;
}
body.print-sizing .page { transform: none !important; }

/* Forcing every .modal-overlay (not just the preview modal) into flow above
   means all of them — including ones never actually opened — would render
   inline and overlap on screen during this pre-warm, without @media print's
   own visibility trick (below) to hide everything but the printable pages.
   Mirroring that trick here turns the pre-warm into a clean "preparing to
   print" flash of the true-size page instead of an overlapping-UI glitch. */
body.print-sizing * { visibility: hidden; }
body.print-sizing .printable-page,
body.print-sizing .printable-page * { visibility: visible; }
body.print-sizing .app-header,
body.print-sizing .controls,
body.print-sizing .column-heading,
body.print-sizing .modal-header,
body.print-sizing .announcement-banner { display: none !important; }
body.print-sizing .preview-sidebar { display: none !important; }
body.print-sizing .leaflet-control-zoom,
body.print-sizing .leaflet-control-attribution,
body.print-sizing .mask-hole,
body.print-sizing .frame-handle { display: none !important; }

/* body itself is a column flexbox the rest of the time (see the top of this
   file) — left as display:flex here, .app and every modal-overlay (all
   siblings of .app, all now in normal flow instead of position:fixed) become
   flex items competing for body's viewport-height main size. flex-shrink
   then collapses .app to 0 height, and the next flex item (whichever modal
   comes right after it in the DOM) starts at the same Y coordinate .app was
   supposed to occupy — painting directly over the front page. Turning off
   body's own flex layout here lets everything just stack in normal block
   flow at its own natural (now true-size) height instead. */
body.print-sizing { display: block; }

/* The screen-only rule above (search "Full-screen map chooser") stretches
   #front-page/#front-wrapper to fill .front-composer at 100% for the live
   "drag the map" editor — an ID-selector rule, so it still outranks the true
   mm sizing above by specificity even under .print-sizing. That's harmless
   once real printing starts (that rule is @media screen-only, so it stops
   applying), but during this pre-warm — deliberately still on-screen —
   it's still active, and with .front-composer's own height now auto, a
   100%-height child has nothing left to resolve against and collapses to 0.
   These four (one per paper size/orientation) restore the same true mm size
   .page.size-*.orient-* sets, just specific enough to actually win here. */
body.print-sizing #front-page.size-a4.orient-portrait  { width: 210mm !important; height: 297mm !important; padding: 5mm !important; }
body.print-sizing #front-page.size-a4.orient-landscape { width: 297mm !important; height: 210mm !important; padding: 5mm !important; }
body.print-sizing #front-page.size-a3.orient-portrait  { width: 297mm !important; height: 420mm !important; padding: 5mm !important; }
body.print-sizing #front-page.size-a3.orient-landscape { width: 420mm !important; height: 297mm !important; padding: 5mm !important; }

@media print {
  body * { visibility: hidden; }
  .printable-page, .printable-page * { visibility: visible; }

  .app-header, .controls, .column-heading, .modal-header, .announcement-banner { display: none !important; }
  .preview-sidebar { display: none !important; }

  body, .app, .layout { background: #fff; margin: 0; padding: 0; }

  /* body is a column flexbox the rest of the time — left as display:flex
     here, .app and every modal-overlay (siblings of .app, now in normal flow
     instead of position:fixed) become flex items competing for body's
     viewport-height main size, and flex-shrink collapses .app to 0 height —
     the next flex item (whichever modal comes after it in the DOM) then
     starts at that same Y coordinate, painting directly over the front page. */
  body { display: block; }

  /* .app is a flex column and .layout is flex:1 — left alone, .layout would
     stretch to fill the viewport height even when its content (e.g. an emptied
     .front-composer, while the preview modal is open) doesn't need the space,
     producing a leading blank page. */
  .app { display: block; height: auto; }
  .layout { padding: 0; display: block; height: auto; }

  /* See the matching rule (and its longer explanation) in the .print-sizing
     pre-warm block above — a closed modal-overlay is still
     display:flex/position:fixed (not display:none) the rest of the time, so
     without this it would be forced into flow by the rule below along with
     everything else here, and its real (if invisible) height stacks up after
     the printable pages, enough in practice to overflow onto its own extra
     blank page. #preview-modal is excluded since #back-wrapper lives inside
     it permanently and must stay in flow to print even when never opened. */
  .modal-overlay:not(.open):not(#preview-modal) { display: none !important; }

  /* Whichever container currently holds the printable pages — the live composer,
     or the preview modal if it happened to be open — gets reset to normal flow. */
  .front-composer,
  .modal-overlay, .modal-box, .preview-modal-content, .modal-body {
    display: block;
    position: static;
    width: auto;
    height: auto;
    max-width: none;
    max-height: none;
    margin: 0;
    padding: 0;
    background: none;
    border: none;
    box-shadow: none;
    opacity: 1;
    overflow: visible;
  }

  /* Both panes print regardless of which tab was active on screen. */
  .modal-pane { display: block !important; }

  #front-wrapper { page-break-after: always; }

  .page-wrapper {
    box-shadow: none;
    width: auto !important;
    height: auto !important;
    overflow: visible;
  }

  .page {
    transform: none !important;
    box-shadow: none;
  }

  .leaflet-control-zoom,
  .leaflet-control-attribution {
    display: none !important;
  }

  .map-frame {
    border: 1px solid var(--navy);
  }

  .mask-hole,
  .frame-handle {
    display: none;
  }
}

/* Without this, the browser's own default print margins (~1cm on every
   edge in Chrome) stack on top of the .page element's own true 210mm/297mm
   etc size above — the total no longer fits the physical sheet, so the
   printer/browser silently scales the whole page down to compensate. That
   shrinks the map too, so the scale bar and the stated map scale (both
   computed assuming the page prints at its true, unscaled physical size) no
   longer match what actually comes out, and leaves the map surrounded by
   more blank paper than the front page's own 5mm padding accounts for.
   Zeroing the page margin here removes the browser's own margin box
   entirely, so the .page element's true size is the whole sheet. */
@page {
  margin: 0;
}
