/* =========================================================
   Avery Label Generator - Creative Playroom Studio Theme
   ========================================================= */

/* Reset & Base Styles */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Pinned label font (see .label-cell below), vendored as a single variable
   woff2 rather than static Regular+Bold files — its wght axis (200-800)
   covers both weights the Weight control offers, and the one file is
   smaller than the two static files combined (48KB vs ~50KB). Upright only;
   no italic is offered anywhere in the UI. SIL OFL 1.1 — see
   fonts/AtkinsonHyperlegibleNext-OFL.txt.
   Source: https://github.com/googlefonts/atkinson-hyperlegible-next */
@font-face {
  font-family: "Atkinson Hyperlegible Next";
  src: url("../fonts/AtkinsonHyperlegibleNext-Variable.woff2") format("woff2");
  font-weight: 200 800;
  font-style: normal;
  font-display: swap;
}

/* UI display face — headings, buttons, the mascot bubble. Vendored as a
   variable woff2 with its wdth axis dropped (Fredoka ships wght+wdth; only
   wght is ever used here, and instancing the width axis out roughly halves
   the file before subsetting). Subset to Latin + Latin Extended, upright
   only. SIL OFL 1.1 — see fonts/Fredoka-OFL.txt.
   Source: https://github.com/hafontia-zz/Fredoka-One
   Unlike the label font, nothing measures this text, so font-display: swap
   is fine — a brief fallback-font flash costs nothing here. */
@font-face {
  font-family: "Fredoka";
  src: url("../fonts/Fredoka-Variable.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* Two-font UI split: --font-display (Fredoka) is for fixed, short UI
   strings only — the app title, section headings, buttons, the mascot
   bubble. --font-ui (Atkinson Hyperlegible Next, the same face pinned on
   .label-cell) is the default for everything else, and that's not just a
   style choice: anything that can display the user's own CSV data (column
   names in the mapping selects, legend values, manual-entry text) MUST use
   it, since Fredoka's vendored subset doesn't cover the diacritics real
   rosters contain — Fredoka never shows anything but our own fixed strings. */
:root {
  --font-ui: "Atkinson Hyperlegible Next", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-display: "Fredoka", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* How far the title logo's right edge spills past .controls-panel's own
     border, into the label area's gutter (see .title-logo below). Also
     drives .sheet-container's left padding, so the reserved gutter always
     covers the spill exactly — change this one value to retune both. */
  --title-spill: 50px;

  /* "Parchment & Gold" castle palette, lifted from the title logo. Every UI
     color routes through one of these — a new hex value anywhere outside
     this block (other than the sort-tag/color-bar data palette in main.js,
     and the white/black overlay gradients on those shapes, which are the
     user's data colors and stay independent of this theme) means the
     palette drifted somewhere it shouldn't have. For translucent variants
     (glows, tinted shadows), use color-mix(in srgb, var(--token) N%,
     transparent) against one of these rather than a new rgba() literal. */
  --ink: #2b2d3b;          /* text; from the logo's pixel outlines */
  --ink-soft: #5a5e6e;     /* help text, secondary text */
  --paper: #ffffff;        /* sidebar, inputs, label sheets */
  --parchment: #fbf6ea;    /* page background, cards */
  --dot: #e3d6b3;          /* label-area dot grid */
  --stone-50: #f5f6f9;
  --stone-100: #eceef3;
  --stone-300: #c5cad6;
  --stone-500: #8b91a2;
  --stone-700: #5b6072;
  --stone-700-hover: #666b7e;
  --stone-800: #454857;
  --gold: #f2c65c;         /* banner trim */
  --gold-hover: #f5d072;
  --gold-deep: #c9962f;
  --banner: #ee8a3c;       /* "CASTLE" lettering */
  --banner-hover: #f19a55;
  --banner-deep: #b0561f;
  /* Darkened ~3% from the given #1f7f76 — as specified, its text use
     (.page-summary strong on --parchment) measured 4.47:1, just under the
     4.5:1 minimum. This is the smallest shift that clears it (4.71:1),
     visually indistinguishable from the original. Flag it if that's wrong. */
  --flag: #1e7b72;         /* the teal flag */
  /* Darkened ~3% from the given #d64545 — as specified, white text on it
     (the overflow badge) measured 4.38:1, just under 4.5:1. Smallest shift
     that clears it (4.60:1); same reasoning as --flag above. */
  --alert: #d04343;        /* overflow warning ONLY */
  /* A distinct hue from every other token here on purpose: a barcode that
     silently couldn't be encoded (see .label-cell.barcode-unencodable) is a
     different problem from overflow, and reusing --alert would blur that
     distinction. #7c4dbe clears 4.5:1 against white at 5.73:1 already, no
     darkening needed the way --flag/--alert above required. */
  --barcode-warning: #7c4dbe;
  --label-outline: #e9cf85; /* on-screen label cell outline (print removes it) */
  /* Printed label ink — deliberately NOT part of the theme above. .label-cell
     set no color of its own pre-theme, so it inherited body's — meaning
     recoloring body's text here would have silently recolored every printed
     label along with the UI. Pinned to the exact pre-theme value instead, so
     printed output stays identical regardless of future UI theme changes. */
  --label-ink: #4a3e3d;

  /* The castle-wall divider between the sidebar's parts (top bar, the
     numbered sections, the footer): a 16x10 pixel-art tile, drawn at 2x
     with nearest-neighbour scaling and repeated across. Its colours are the
     title logo's stone and live in the image, like the logo's. Centred, so
     both ends match whatever the panel's width (it narrows when the
     scrollbar shows). */
  --wall-divider: url("../img/wall-divider.png") center top / 32px 20px repeat-x;
  --wall-divider-height: 20px;

  /* The stone wall the sidebar is built from: a 32x16 pixel-art tile of
     light-grey blocks (colours in the image, like the divider's), drawn at
     2x and repeated. `fixed` pins it to the viewport, and since the panel
     itself is fixed at the top left, that's the panel's own grid: the
     footer wall's gaps (below) are filled from the same grid and line up
     with the stone behind them exactly. The controls never sit on it
     directly; they're on paper cards (.sidebar-section). */
  --stone-wall: url("../img/stone-wall.png") 0 0 / 64px 32px repeat fixed;
}

body {
  font-family: var(--font-ui);
  background-color: var(--parchment);
  color: var(--ink);
  display: flex;
  min-height: 100vh;
}

/* App Controls Sidebar: a three-part column. A non-scrolling header (the
   title logo, then .top-bar), an independently-scrolling body
   (.controls-scroll below) and a non-scrolling footer (.controls-footer:
   page summary, Printer alignment, Print). A single overflow-y:auto panel,
   as this used to be, clips anything that sticks out of it — and the whole
   point of the logo below is that it doesn't get clipped, so the panel
   itself has to stop being the scroll container. The footer is a sibling
   of the body, not laid over it, so it can never cover the body's last
   controls. */
.controls-panel {
  width: 360px;
  background: var(--paper); /* under the stone, in case the tile fails to load */
  padding-top: 20px;
  border-right: 3px dashed var(--gold);
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  overflow: visible;
  box-shadow: 6px 0 24px color-mix(in srgb, var(--gold-deep) 12%, transparent);
  z-index: 100;
}

/* The stone, on its own layer behind the panel's content rather than as
   the panel's background, because it needs image-rendering: pixelated and
   that property inherits: set on the panel, it would also apply to the
   title logo, which is downscaled 2:1 and must stay smooth. z-index -1
   keeps it inside the panel's own stacking context (the panel has
   z-index: 100), above the panel's paper but under everything else. */
.controls-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--stone-wall);
  image-rendering: pixelated;
  pointer-events: none;
}

/* Scrolls independently beneath the fixed logo header, so the controls slide
   under it rather than up behind its transparent gaps. Owns the column gap
   and the side/bottom padding that used to live directly on .controls-panel
   — the logo needs its own positioning instead (see .title-logo), since
   this padding would otherwise constrain it too. */
.controls-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 14px; /* between the section cards */
  /* Narrower sides than the top bar's 20px: the cards' own border and
     padding take the difference, so the controls inside them keep nearly
     their old width (313px against 317px), and nothing wraps. The bottom
     24px is what the footer's wall stands over. */
  padding: 12px 10px 24px;
}

/* Custom Soft Scrollbar */
.controls-scroll::-webkit-scrollbar {
  width: 8px;
}
.controls-scroll::-webkit-scrollbar-track {
  background: var(--stone-100);
  border-radius: 10px;
}
.controls-scroll::-webkit-scrollbar-thumb {
  background: var(--gold);
  border-radius: 10px;
}
.controls-scroll::-webkit-scrollbar-thumb:hover {
  background: var(--gold-deep);
}

/* Header: the stylized banner logo. flex-shrink:0 keeps its 121px height
   fixed even in a pathologically short viewport, so .controls-scroll below
   is always the one that gives up space, never the logo. */
.controls-panel h1 {
  flex-shrink: 0;
}

/* Top bar: Undo, Redo, Save, Load and the More disclosure, fixed under the
   logo. The More summary is positioned into the button row (the buttons
   leave room for it on the right), so it stays put when its panel opens
   below the row; the panel then flows in the header, pushing the body
   down rather than covering it. */
.top-bar {
  flex-shrink: 0;
  position: relative;
  padding: 6px 20px calc(10px + var(--wall-divider-height));
}

.top-bar::after,
.controls-footer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: var(--wall-divider-height);
  background: var(--wall-divider);
  image-rendering: pixelated;
  pointer-events: none;
}

.top-bar::after {
  bottom: 0;
}

.top-bar-buttons {
  display: flex;
  gap: 6px;
  padding-right: 76px; /* room for the More summary */
}

.top-bar-buttons button.secondary-sm {
  flex: 1;
  padding: 6px 4px;
  white-space: nowrap;
}

.more-menu > summary {
  position: absolute;
  top: 6px;
  right: 20px;
  width: 70px;
  list-style: none;
  font-family: var(--font-display);
  font-size: 0.78rem;
  font-weight: 700;
  text-align: center;
  white-space: nowrap;
  padding: 6px 4px;
  color: var(--stone-800);
  background-color: var(--stone-100);
  border: 2px solid var(--stone-300);
  border-radius: 10px;
  box-shadow: 0 2px 0 var(--stone-300);
  cursor: pointer;
}

.more-menu > summary::-webkit-details-marker {
  display: none;
}

.more-menu > summary:hover,
.more-menu[open] > summary {
  background-color: var(--stone-300);
}

.more-menu > summary:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.more-panel {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 10px;
  max-height: 40vh;
  overflow-y: auto;
  padding: 12px;
  background: var(--parchment);
  border: 2px dashed var(--gold);
  border-radius: 14px;
}

/* "Restored your last unsaved session." Floats just under the top bar for
   its few seconds instead of taking a line in the layout, so the controls
   never jump when it appears and disappears. pointer-events: none lets a
   click land on whatever it happens to cover. */
.autosave-toast {
  position: absolute;
  top: 100%;
  left: 20px;
  right: 20px;
  margin-top: 6px;
  z-index: 2;
  pointer-events: none;
  padding: 6px 12px;
  border-radius: 12px;
  background: var(--flag);
  color: var(--paper);
  font-size: 0.8rem;
  font-weight: 700;
  text-align: center;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--ink) 25%, transparent);
}

/* The numbered groups in .controls-scroll (Sheet, Content, Design, Sort &
   colour), each a paper card on the stone wall (see --stone-wall), in the
   raised style of the app's buttons. Every word in the sidebar sits on
   paper or on a control of its own, never on the stone itself;
   tests/sidebar.spec.js checks that. There are no <hr>s in the sidebar. */
.sidebar-section,
.controls-footer .sidebar-details {
  background: var(--paper);
  border: 2px solid var(--stone-300);
  border-radius: 14px;
  box-shadow: 0 4px 0 var(--stone-300);
}

.sidebar-section {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 14px 10px 16px;
}

/* The numbered headings are pennant flags on poles, echoing the title
   logo's banner: "CASTLE" orange cloth, a gold band inside a 2px ink
   outline, a swallowtail notch at the fly end, and stone-50 lettering with
   a 2px ink pixel outline. The h2 itself is unclipped and holds only the
   text (its textContent and the sections' aria-labelledby names are
   unchanged). The rest is decoration:
   - ::before is the pole, ::after the gold finial on top of it;
   - .flag-cloth (an empty aria-hidden span) is the cloth, in three
     clip-path layers: the span is the ink outline, its ::before the gold
     band, its ::after the orange. Each layer is inset by its band width,
     and on the notch's diagonals the inset is measured square to the
     slant, so the outline and gold band keep their thickness all the way
     round the notch (see the offsets below).
   Contrast: stone-50 on --banner alone is only about 2.5:1. The solid 2px
   ink outline around every letter is what makes it readable, as on the
   logo; don't thin it or blur it. */
.section-heading {
  --flag-top: 8px;    /* finial and pole start above the cloth */
  --flag-h: 40px;     /* the cloth's height, outline included */
  --flag-notch: 16px; /* swallowtail depth at the outer edge */
  --pole-x: 3px;      /* centres the 8px pole under the 14px finial */
  --pole-w: 8px;
  position: relative;
  isolation: isolate; /* keeps the cloth's z-index: -1 inside the heading */
  align-self: flex-start;
  max-width: 100%;
  /* Text sits on the cloth: 17px in from the pole-side edge, clear of the
     notch on the right. line-height is the cloth's height, so the text is
     centred on it; the bottom padding is the pole below the cloth. */
  padding: var(--flag-top) calc(var(--flag-notch) + 22px) 8px
    calc(var(--pole-x) + var(--pole-w) + 17px);
  font-family: var(--font-display);
  font-size: 1.15rem;
  line-height: var(--flag-h);
  font-weight: 700; /* Fredoka's heaviest weight — don't rely on the browser clamping 800 */
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
  color: var(--stone-50);
  text-shadow:
    2px 0 var(--ink), -2px 0 var(--ink), 0 2px var(--ink), 0 -2px var(--ink),
    2px 2px var(--ink), -2px 2px var(--ink), 2px -2px var(--ink), -2px -2px var(--ink);
}

/* The pole: from just under the finial to 8px below the cloth. */
.section-heading::before {
  content: "";
  position: absolute;
  left: var(--pole-x);
  top: 6px;
  bottom: 0;
  width: var(--pole-w);
  background: var(--ink);
  border-left: 2px solid var(--stone-700);
}

/* The finial: a gold knob on the pole's top (the one rounded shape). */
.section-heading::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 10px;
  height: 10px;
  background: var(--gold);
  border: 2px solid var(--ink);
  border-radius: 50%;
}

.section-heading .flag-cloth,
.section-heading .flag-cloth::before,
.section-heading .flag-cloth::after {
  position: absolute;
  inset: 0;
}

/* Outer layer: the 2px ink outline. Butts up against the pole. */
.section-heading .flag-cloth {
  inset: var(--flag-top) 0 auto calc(var(--pole-x) + var(--pole-w));
  height: var(--flag-h);
  z-index: -1;
  background: var(--ink);
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--flag-notch)) 50%, 100% 100%, 0 100%);
}

/* The inner layers' notch offsets. Insetting a band of width t square to
   the notch's slant moves each diagonal left by s = t * sqrt(1 + (2D/H)^2),
   D the notch depth and H the cloth height; the top/bottom corners then
   sit a further 2Dt/H left, where the moved diagonal meets the inset edge.
   With D = 16px and H = 40px, sqrt(1 + 0.8^2) = 1.2806, so:
     t = 2px (inside the ink):  corners 100% - 4.16px,  notch 100% - 18.56px
     t = 5px (inside the gold): corners 100% - 10.40px, notch 100% - 22.40px
   Recompute these if --flag-notch or --flag-h ever changes. */
.section-heading .flag-cloth::before {
  content: "";
  background: var(--gold);
  clip-path: polygon(2px 2px, calc(100% - 4.16px) 2px, calc(100% - 18.56px) 50%,
    calc(100% - 4.16px) calc(100% - 2px), 2px calc(100% - 2px));
}

.section-heading .flag-cloth::after {
  content: "";
  background: var(--banner);
  clip-path: polygon(5px 5px, calc(100% - 10.4px) 5px, calc(100% - 22.4px) 50%,
    calc(100% - 10.4px) calc(100% - 5px), 5px calc(100% - 5px));
}

/* A sub-group inside Design (Text, Fixed text, Logo). */
.design-group {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* A control group that only means something once there's a value to act
   on (the fixed text's size and position once there's text, the logo's
   once one is uploaded). Collapsed, it stays in the DOM and in the
   accessibility tree, just clipped to nothing: its controls are disabled at
   the same time (see updatePlacementPickers() in main.js), so nothing
   inside can be focused. The negative margin cancels the gap its parent
   would otherwise leave for it. */
.reveal-when-set {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.reveal-when-set.is-collapsed {
  max-height: 0;
  overflow: hidden;
  margin-top: -12px;
}

/* Collapsible sidebar groups (Background, Printer alignment): native
   <details>. Open/closed is view state only, never saved. The "on" marker
   shows while the group holds a non-default value (see
   syncSidebarDisclosures() in main.js). */
.sidebar-details > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  list-style: none;
  cursor: pointer;
}

.sidebar-details > summary::-webkit-details-marker {
  display: none;
}

.sidebar-details > summary::before {
  content: "▸";
  color: var(--banner-deep);
  font-size: 0.8rem;
  transition: transform 0.15s ease;
}

.sidebar-details[open] > summary::before {
  transform: rotate(90deg);
}

.sidebar-details > summary:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: 4px;
}

.controls-panel .sidebar-details > summary h3 {
  margin-top: 0;
}

.sidebar-details[open] > summary {
  margin-bottom: 12px;
}

.on-marker {
  font-family: var(--font-ui);
  font-size: 0.7rem;
  font-weight: 800;
  color: var(--paper);
  background: var(--flag);
  padding: 1px 8px;
  border-radius: 10px;
}

button.clear-sheet-btn {
  align-self: flex-start;
}

/* Footer: pinned below the scrolling body. */
.controls-footer {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  position: relative;
  padding: 10px 10px 14px;
}

/* Printer alignment gets a small card of its own: it's the one piece of
   footer text that would otherwise sit on the stone. */
.controls-footer .sidebar-details {
  padding: 8px 12px;
}

/* The footer's wall stands on top of it, over the scrolling body's last
   20px. That's inside .controls-scroll's 24px bottom padding, so it never
   covers a control. The gaps between the merlons are filled with the
   stone (same fixed grid as the panel's, so it lines up) rather than left
   see-through, so cards scrolling past don't show through them in
   fragments. */
.controls-footer::before {
  top: calc(-1 * var(--wall-divider-height));
  background: var(--wall-divider), var(--stone-wall);
}

/* The render notices can run to a few lines; cap them so they can't push
   the body to nothing on a short screen. */
.footer-notices {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 30vh;
  overflow-y: auto;
}

.footer-notices:not(:has(> :not(.hidden))) {
  display: none;
}

.footer-notices .render-issue-notice {
  margin-top: 0;
}

/* "Fill in labels", after a cancelled autosave restore: on its own line
   under the notice's text. */
.render-cancel-fill-btn {
  display: block;
  margin-top: 6px;
}

.controls-footer .sidebar-details .control-section {
  max-height: 40vh;
  overflow-y: auto;
}

.footer-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.footer-row .page-summary {
  flex: 1;
  min-width: 0;
}

.footer-row #printBtn {
  flex-shrink: 0;
}

/* Sized so its right edge crosses the dashed border into the label area's
   gutter — see --title-spill above, and .sheet-container's padding-left
   below, which reserves exactly enough room so the sheets can never end up
   under it. margin-right doesn't change what's painted (nothing here clips
   it), just makes the amount it spills by an explicit, tunable value tied
   to the same variable .sheet-container uses, instead of an incidental
   side effect of the image simply being wider than its container. */
.title-logo {
  display: block;
  width: 400px; /* 2x the intended on-screen size, for sharp screens */
  height: 121px;
  margin-left: 12px; /* ~8px left of .controls-scroll's 20px content edge — a small bleed to match the banner's tapered shape */
  margin-right: calc(-1 * var(--title-spill));
}

.controls-panel h3 {
  font-family: var(--font-display);
  font-size: 0.85rem;
  font-weight: 700; /* Fredoka's heaviest weight — don't rely on the browser clamping 800 */
  color: var(--banner-deep);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-top: 2px;
}

.help-text {
  font-family: var(--font-ui);
  font-size: 0.82rem;
  color: var(--ink-soft);
  line-height: 1.45;
}

/* Screen-reader-only live region (the "Saved data cleared." announcement) —
   present for assistive tech to find, invisible to sighted users. Not
   display:none, which would also hide it from the accessibility tree. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.saved-data-status {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.clear-saved-data-confirm {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.saved-data-details summary {
  font-family: var(--font-ui);
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--stone-700);
  cursor: pointer;
}

.saved-data-details[open] summary {
  margin-bottom: 6px;
}

/* Page summary badge ("2 Sheets · 45 Total Labels"), beside Print in the
   footer. */
.page-summary {
  font-family: var(--font-ui);
  font-size: 0.82rem;
  background: var(--parchment);
  color: var(--ink);
  padding: 8px 10px;
  border-radius: 12px;
  border: 2px solid var(--gold);
  text-align: center;
  font-weight: 700;
  box-shadow: 0 3px 0 var(--gold);
}

.page-summary strong {
  color: var(--flag);
}

/* Center-screen overlay shown only while a chunked CSV render (see
   runChunkedPopulate in main.js) has been running long enough to be worth
   mentioning — see showRenderProgress()/hideRenderProgress() there for the
   show-delay / minimum-lifetime timing this depends on. Fixed over the
   whole viewport (a sibling of .controls-panel and .sheet-container in the
   markup, not nested in either), so neither panel's own z-index or scroll
   context matters. The scrim is deliberately translucent, not a heavy
   blackout — cells visibly filling in behind it during a real render is
   itself feedback that the app hasn't frozen. */
.render-progress-overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: color-mix(in srgb, var(--ink) 50%, transparent);
}

/* "Drop your CSV to load it", while a file is dragged over the window (see
   the drag-and-drop handlers in main.js). Fixed, so it takes no layout space
   and can't shift the sheet or sidebar, and pointer-events: none, so the
   drop lands on what's underneath it (a file dropped on the logo or
   background input must still reach that input). Above the progress
   overlay, though main.js never shows both at once. */
.drop-overlay {
  position: fixed;
  inset: 0;
  z-index: 1100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  pointer-events: none;
  background: color-mix(in srgb, var(--parchment) 70%, transparent);
  outline: 4px dashed var(--gold-deep);
  outline-offset: -14px;
}

.drop-overlay-card {
  padding: 24px 32px;
  background: var(--paper);
  border: 3px solid var(--gold);
  border-radius: 20px;
  box-shadow: 0 16px 40px color-mix(in srgb, var(--ink) 30%, transparent);
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 700;
  color: var(--banner-deep);
  text-align: center;
}

.render-progress-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  width: 100%;
  max-width: 380px;
  padding: 28px 32px;
  background: var(--paper);
  border: 3px solid var(--gold);
  border-radius: 20px;
  box-shadow: 0 16px 40px color-mix(in srgb, var(--ink) 30%, transparent);
  text-align: center;
}

.render-progress-heading {
  font-family: var(--font-display);
  font-size: 1.2rem;
  font-weight: 700; /* Fredoka's heaviest weight — don't rely on the browser clamping 800 */
  color: var(--banner-deep);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.render-progress-phase {
  font-family: var(--font-ui);
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--ink);
}

.render-progress-count {
  font-family: var(--font-ui);
  font-size: 0.8rem;
  color: var(--ink-soft);
}

/* Room for a mascot illustration to sit beside the bar later (art doesn't
   exist yet, so nothing is placed here today) — the row layout already
   accommodates one without needing to be restructured when it arrives. */
.render-progress-visual {
  display: flex;
  align-items: center;
  gap: 16px;
  width: 100%;
}

/* Same visual language as a normal track/bar, just larger — the old 8px
   sidebar track reads as a hairline at this size. */
.render-progress-track {
  flex: 1;
  height: 18px;
  border-radius: 10px;
  background: var(--stone-100);
  border: 2px solid var(--stone-300);
  overflow: hidden;
}

.render-progress-bar {
  height: 100%;
  background: var(--gold);
  transition: width 0.1s linear;
}

/* Settle-phase indeterminate treatment (see waitForQrSettle/
   enterRenderSettlePhase in main.js): the bar sits pinned at 100% for up to
   ~1s while trailing QR swaps resolve, and a static full bar for a whole
   second reads as a hang at this size — a subtle shimmer says "still
   working" instead of trying to fold the wait into the percentage math.
   Dropped under prefers-reduced-motion below, relying on the "Finishing
   up…" text change alone. */
.render-progress-bar.is-settling {
  background-image: linear-gradient(90deg, var(--gold) 0%, var(--gold-hover) 50%, var(--gold) 100%);
  background-size: 200% 100%;
  animation: render-progress-shimmer 1.2s ease-in-out infinite;
}

@keyframes render-progress-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .render-progress-bar.is-settling {
    animation: none;
    background-image: none;
  }
}

/* Mode Switcher Tabs */
.mode-toggle {
  display: flex;
  background: var(--stone-100);
  padding: 6px;
  border-radius: 18px;
  gap: 6px;
  border: 2px solid var(--stone-300);
}

.mode-btn {
  flex: 1;
  padding: 8px 12px;
  font-size: 0.85rem;
  background: transparent;
  color: var(--ink-soft);
  border-radius: 12px;
  border: none;
  cursor: pointer;
  font-weight: 700; /* Fredoka's heaviest weight — don't rely on the browser clamping 800 */
  transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.mode-btn.active {
  background: var(--paper);
  color: var(--banner-deep);
  box-shadow: 0 4px 10px color-mix(in srgb, var(--ink) 20%, transparent);
  transform: scale(1.02);
}

.control-section {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.hidden {
  display: none !important;
}

.input-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.flex-row {
  flex-direction: row;
  gap: 12px;
}

.flex-1 {
  flex: 1;
}

.input-group label {
  font-family: var(--font-ui);
  font-size: 0.8rem;
  font-weight: 800;
  color: var(--stone-700);
  letter-spacing: 0.02em;
}

/* Form Controls */
textarea, input[type="file"], input[type="number"] {
  width: 100%;
  padding: 10px 14px;
  border: 2.5px solid var(--stone-300);
  border-radius: 14px;
  font-family: var(--font-ui);
  font-size: 0.88rem;
  background: var(--paper);
  color: var(--ink);
  transition: all 0.2s ease;
}

textarea:focus, input[type="number"]:focus {
  outline: none;
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--gold) 35%, transparent);
}

select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  width: 100%;
  padding: 10px 36px 10px 14px;
  border: 2.5px solid var(--stone-300);
  border-radius: 14px;
  font-family: var(--font-ui);
  font-size: 0.88rem;
  font-weight: 700;
  color: var(--ink);
  background-color: var(--paper);
  /* Data URIs can't reference CSS custom properties, so this chevron's
     color (%235b6072 = --stone-700) is a literal that has to be kept in
     sync by hand if --stone-700 ever changes — the one color in this file
     that can't route through a var(). */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%235b6072' stroke-width='3'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 16px 16px;
  cursor: pointer;
  transition: all 0.2s ease;
}

select:hover {
  border-color: var(--gold);
  background-color: var(--paper);
}

/* Muted like a disabled placement picker (the logo's Size select, until a
   logo is uploaded). */
select:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

select:focus {
  outline: none;
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--gold) 35%, transparent);
}

select option {
  font-family: var(--font-ui); /* explicit, not just inherited from select — some browsers render the native option popup outside normal cascade */
  padding: 8px;
  font-weight: 700;
  color: var(--ink);
}

/* Placement pickers (Column Mapping, see buildPlacementPickers() in
   main.js): a tiny 3x3 "map of the label" per element, not a toolbar, so
   they drop the chunky button look entirely. The selected zone is filled
   with --banner-deep, the same "selected" color as the active mode tab
   (5.0:1 against --paper, 4.3:1 against the --stone-100 cells beside it).
   The keyboard focus ring is --ink (well past 4.5:1 on --paper), with the
   same gold glow every other control shows on the picker as a whole. A
   faint --stone-500 dot marks a zone another mapped element already sits
   in, so users can see what they'd be stacking with. */
.select-with-picker {
  display: flex;
  align-items: center;
  gap: 10px;
}

.select-with-picker select {
  flex: 1;
  min-width: 0;
}

.placement-row {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.placement-row-label {
  font-family: var(--font-ui);
  font-size: 0.8rem;
  font-weight: 800;
  color: var(--stone-700);
  letter-spacing: 0.02em;
}

.zone-picker {
  flex-shrink: 0;
  display: grid;
  grid-template-columns: repeat(3, 18px);
  grid-template-rows: repeat(3, 18px);
  gap: 2px;
  padding: 3px;
  border: 2px solid var(--stone-300);
  border-radius: 8px;
  background: var(--paper);
  transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.zone-picker:focus-within {
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--gold) 35%, transparent);
}

.zone-picker.is-disabled {
  opacity: 0.45;
}

/* "button." on each selector keeps these ahead of the generic button,
   button:hover and button:disabled:hover rules above. */
.zone-picker button.zone-picker-cell {
  position: relative;
  padding: 0;
  border: 1px solid var(--stone-300);
  border-radius: 3px;
  background-color: var(--stone-100);
  box-shadow: none;
  transform: none;
  transition: background-color 0.15s ease;
}

.zone-picker button.zone-picker-cell:hover {
  background-color: var(--gold-hover);
  box-shadow: none;
  transform: none;
}

.zone-picker button.zone-picker-cell[aria-checked="true"] {
  background-color: var(--banner-deep);
  border-color: var(--banner-deep);
}

.zone-picker button.zone-picker-cell:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
  z-index: 1;
}

.zone-picker button.zone-picker-cell:disabled {
  opacity: 1; /* the whole picker is muted instead, see .is-disabled */
  cursor: not-allowed;
  background-color: var(--stone-100);
}

.zone-picker button.zone-picker-cell[aria-checked="true"]:disabled {
  background-color: var(--banner-deep);
}

.zone-picker button.zone-picker-cell.has-other::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
  background: var(--stone-500);
}

.zone-picker button.zone-picker-cell[aria-checked="true"].has-other::after {
  background: var(--paper);
}

/* Layout presets (above the placement pickers, see buildPlacementPresets()
   in main.js): small toggle buttons in the .secondary-sm style. The pressed
   preset reuses the pickers' selected color, --banner-deep, with --paper
   text (5.0:1), so "this is the layout you have" reads the same in both
   controls. Focus ring as on the picker cells. */
.placement-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.placement-presets button.placement-preset-btn {
  flex: 1 1 0;
  min-width: max-content;
  padding: 6px 10px;
  font-size: 0.78rem;
  color: var(--stone-800);
  background-color: var(--stone-100);
  border: 2px solid var(--stone-300);
  border-radius: 10px;
  box-shadow: 0 2px 0 var(--stone-300);
}

.placement-presets button.placement-preset-btn:hover {
  background-color: var(--stone-300);
  transform: translateY(-1px);
  box-shadow: 0 2px 0 var(--stone-300);
}

.placement-presets button.placement-preset-btn[aria-pressed="true"] {
  color: var(--paper);
  background-color: var(--banner-deep);
  border-color: var(--banner-deep);
  box-shadow: none;
  transform: none;
}

.placement-presets button.placement-preset-btn:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

input[type="file"] {
  padding: 8px;
  background: var(--stone-50);
  cursor: pointer;
}

input[type="file"]::-webkit-file-upload-button {
  font-family: var(--font-display); /* this pseudo-element isn't a real <button>, so it doesn't inherit from the button rule below */
  background: var(--gold);
  color: var(--ink);
  border: none;
  border-radius: 8px;
  padding: 4px 10px;
  font-weight: 700; /* Fredoka's heaviest weight — don't rely on the browser clamping 800 */
  cursor: pointer;
}

input[type="range"],
input[type="checkbox"] {
  accent-color: var(--gold-deep);
}

/* Interactive Buttons */
button {
  font-family: var(--font-display); /* covers every button variant below (.secondary, .accent, etc.) and .mode-btn, none of which override it */
  padding: 11px 18px;
  font-size: 0.9rem;
  font-weight: 700; /* Fredoka's heaviest weight — don't rely on the browser clamping 800 */
  border: none;
  border-radius: 14px;
  cursor: pointer;
  background-color: var(--gold);
  color: var(--ink);
  transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
  box-shadow: 0 4px 0 var(--gold-deep);
}

button:hover {
  background-color: var(--gold-hover);
  transform: translateY(-2px);
  box-shadow: 0 6px 0 var(--gold-deep);
}

button:active {
  transform: translateY(2px);
  box-shadow: 0 2px 0 var(--gold-deep);
}

button:disabled,
button:disabled:hover {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
  background-color: var(--stone-300);
}

button.secondary {
  background-color: var(--stone-700);
  color: var(--paper);
  box-shadow: 0 4px 0 var(--stone-800);
}

button.secondary:hover {
  background-color: var(--stone-700-hover);
  box-shadow: 0 6px 0 var(--stone-800);
}

button.secondary-sm {
  background-color: var(--stone-100);
  color: var(--stone-800);
  padding: 6px 12px;
  font-size: 0.78rem;
  border: 2px solid var(--stone-300);
  box-shadow: 0 2px 0 var(--stone-300);
  border-radius: 10px;
}

button.secondary-sm:hover {
  background-color: var(--stone-300);
  transform: translateY(-1px);
}

button.accent {
  background-color: var(--banner);
  color: var(--ink);
  box-shadow: 0 4px 0 var(--banner-deep);
}

button.accent:hover {
  background-color: var(--banner-hover);
  box-shadow: 0 6px 0 var(--banner-deep);
}

/* Install button (PWA, see index.html/src/main.js): --flag rather than
   --gold/--banner/--stone, so it reads as a distinct, occasional action
   rather than blending into Save/Load/Undo — the same teal already used
   for the mascot bubble and page-summary highlight, not a new hex value.
   No --flag-deep token exists, so its shadow is derived from --flag itself
   via color-mix rather than inventing one. */
button.install-app-btn {
  width: 100%;
  background-color: var(--flag);
  color: var(--paper);
  box-shadow: 0 4px 0 color-mix(in srgb, var(--flag) 65%, black);
}

button.install-app-btn:hover {
  background-color: color-mix(in srgb, var(--flag) 88%, white);
  box-shadow: 0 6px 0 color-mix(in srgb, var(--flag) 65%, black);
}

/* Main Workspace Canvas */
.sheet-container {
  margin-left: 360px;
  /* Left padding reserves room for the title logo's tail (see
     --title-spill and .title-logo) so the sheets never sit under it, at
     rest or scrolled, at any width. The print stylesheet zeroes this back
     out — see the @media print block below. */
  padding: 40px 40px 40px calc(var(--title-spill) + 30px);
  flex-grow: 1;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  overflow-x: auto;
  background-image: radial-gradient(var(--dot) 1.5px, transparent 1.5px);
  background-size: 20px 20px;
}

.sheets-wrapper {
  display: flex;
  flex-direction: column;
  gap: 36px;
  position: relative;
  transition: transform 0.15s ease-out;
  transform-origin: top left;
}

/* Base Sheet Styles */
.label-sheet {
  width: 8.5in;
  height: 11in;
  background: var(--paper);
  border-radius: 16px;
  box-shadow: 0 12px 30px color-mix(in srgb, var(--stone-700) 15%, transparent), 0 0 0 8px color-mix(in srgb, var(--paper) 80%, transparent);
  display: grid;
  break-after: page;
  page-break-after: always;
  padding: var(--margin-y, 0.5in) var(--margin-x, 0.1875in);
  grid-template-columns: repeat(var(--cols, 3), var(--label-w, 2.625in));
  grid-template-rows: repeat(var(--rows, 10), var(--label-h, 1in));
  column-gap: var(--col-gap, 0.125in);
  row-gap: var(--row-gap, 0in);
}

.label-sheet:last-child {
  break-after: auto;
  page-break-after: auto;
}

/* Base Label Cell Styles */
.label-cell {
  border: 2px dashed var(--label-outline);
  border-radius: 8px;
  overflow: hidden;
  white-space: pre-wrap;
  word-wrap: break-word;
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
  isolation: isolate;
  color: var(--label-ink); /* pinned independently of body's text color — see --label-ink */
  width: var(--label-w, 2.625in);
  height: var(--label-h, 1in);
  padding: var(--pad-y, 0.08in) var(--pad-x-end, 0.12in) var(--pad-y, 0.08in) var(--pad-x-start, 0.22in);
  font-size: var(--label-font-size, 9.5pt);
  line-height: var(--label-line-height, 1.2);
  text-align: var(--label-text-align, left);
  font-weight: var(--label-font-weight, normal);
  /* Pinned independently of the UI (body) font — this is what determines
     wrap points and therefore overflow, so it must be deterministic across
     machines. The generic fallback only applies if the vendored face fails
     to load; font-synthesis: none stops a browser from faking a bold weight
     against that fallback in the meantime, which would measure differently
     than the real face's bold instance once it loads. */
  font-family: "Atkinson Hyperlegible Next", sans-serif;
  font-synthesis: none;
}

/* Background image layer: a pseudo-element rather than a child, because
   .label-cell is contentEditable and its innerHTML is serialized into
   history snapshots and saved project files — a real child node would end
   up inside the user's editable content. `isolation: isolate` above gives
   .label-cell its own stacking context so this z-index:-1 layer stays
   pinned below ALL cell content (including bare text nodes from Manual
   Entry mode, which have no stacking context of their own) while still
   rendering above the cell's own background-color. */
.label-cell::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: var(--label-bg, none);
  background-size: var(--label-bg-size, cover);
  background-repeat: var(--label-bg-repeat, no-repeat);
  background-position: center;
  opacity: var(--label-bg-opacity, 1);
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}

.label-cell:hover {
  background-color: var(--parchment);
  border-color: var(--banner);
}

/* Overflow warning: flags content likely to be clipped when printed. --alert
   is reserved for this — it must never share a color with any button, so
   a flagged label never reads as an actionable control. */
.label-cell.content-overflow {
  outline: 3px solid var(--alert);
  outline-offset: -3px;
}

.label-cell.content-overflow::after {
  content: "✂ oops! clips edge";
  position: absolute;
  bottom: 2px;
  right: 4px;
  font-size: 7.5px;
  font-weight: 800;
  color: var(--paper);
  background: var(--alert);
  padding: 2px 6px;
  border-radius: 8px;
  pointer-events: none;
  z-index: 5;
}

/* A barcode value CODE128 couldn't encode (see canEncodeCode128Value in
   main.js) — a different problem from overflow, so it gets its own token
   (--barcode-warning, never --alert), a dashed rather than solid outline,
   a different icon/wording, and the opposite corner so the two badges
   never overlap if a cell somehow has both problems at once. */
.label-cell.barcode-unencodable {
  outline: 3px dashed var(--barcode-warning);
  outline-offset: -3px;
}

.label-cell.barcode-unencodable::after {
  content: "⚠ no barcode";
  position: absolute;
  bottom: 2px;
  left: 4px;
  font-size: 7.5px;
  font-weight: 800;
  color: var(--paper);
  background: var(--barcode-warning);
  padding: 2px 6px;
  border-radius: 8px;
  pointer-events: none;
  z-index: 5;
}

/* Label content zones (see buildZonesHtml and PLACEMENT_ZONES in main.js,
   and docs/current-state.md, "Label content zones"). A 3-row grid whose
   rows are only emitted when something sits in them: top and bottom are
   `auto` (an absent row is 0 tall), and middle is minmax(0, 1fr) — it takes
   whatever height is left and never grows past it, so middle content that
   is too tall overflows equally off the top and bottom, exactly as the
   pre-zones flex-centered wrapper did. */
.label-zones {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-columns: minmax(0, 1fr);
  width: 100%;
  height: 100%;
}

/* Each row is a flex line of zones in left/center/right order. The 4px gap
   is the old wrapper's gap between the text side and the QR. */
.label-zone-row {
  display: flex;
  gap: 4px;
  min-width: 0;
}

.label-zone-row + .label-zone-row {
  margin-top: 2px;
}

.label-zone-row-top {
  grid-row: 1;
  align-items: flex-start;
}

.label-zone-row-middle {
  grid-row: 2;
  align-items: center;
}

.label-zone-row-bottom {
  grid-row: 3;
  align-items: flex-end;
}

/* A zone holding the text block, the fixed text or a barcode takes the remaining width of
   its row (flex: 1 — the same sizing the old .label-text-side used), so a
   barcode alone in its row gets the full content width. A zone holding
   only fixed-size elements (the QR and/or the logo) is exactly as wide as
   its widest one. Auto margins only place a fixed zone when nothing in its
   row is growing (a growing zone leaves no free space for them):
   center-zone content centered, right-zone flush to the end edge.
   .label-zone-qr-only is what renders before the logo existed called a
   QR-only zone; hand-edited cells saved then still carry it. */
.label-zone {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.label-zone-fixed,
.label-zone-qr-only {
  flex: 0 0 auto;
}

.label-zone-center.label-zone-fixed,
.label-zone-center.label-zone-qr-only {
  margin-inline: auto;
}

.label-zone-right.label-zone-fixed,
.label-zone-right.label-zone-qr-only {
  margin-left: auto;
}

/* A very wide logo would otherwise make its zone wider than the label.
   Capping the zone at its row lets .label-logo's own max-width shrink the
   logo box to fit (the image scales down inside it). Only zones holding a
   logo: a QR-only zone keeps exactly the sizing it always had. */
.label-zone-fixed:has(> .label-logo) {
  max-width: 100%;
}

/* Stacking within a zone: logo, then fixed text, then text, then barcode,
   then QR, 2px apart (the old gap between the text lines and the barcode).
   The text block (and the fixed text) stretches to the zone's width so the text-align control keeps its
   meaning — alignment of the lines within the text block's zone. The logo,
   barcode and QR are fixed-size boxes aligned to their zone's column. */
.label-zone > * + * {
  margin-top: 2px;
}

.label-zone-left > .barcode-1d-svg,
.label-zone-left > .qr-code-box,
.label-zone-left > .label-logo {
  align-self: flex-start;
}

.label-zone-center > .barcode-1d-svg,
.label-zone-center > .qr-code-box,
.label-zone-center > .label-logo {
  align-self: center;
}

.label-zone-right > .barcode-1d-svg,
.label-zone-right > .qr-code-box,
.label-zone-right > .label-logo {
  align-self: flex-end;
}

/* Fixed text (see buildFixedTextHtml in main.js): like the text block it
   spans its zone's width, but its alignment is ALWAYS its zone's column,
   whatever Text Alignment says — there's no control for it, so nothing to
   keep in sync. Font family, weight and line height are the label's (the
   unitless line height scales with the smaller font); only the size
   differs: --label-fixed-text-scale (Small 0.75, Same 1) x the label font
   size, set once on sheetsWrapper. */
.label-fixed-text {
  font-size: calc(var(--label-fixed-text-scale, 0.75) * 1em);
}

.label-zone-left > .label-fixed-text {
  text-align: left;
}

.label-zone-center > .label-fixed-text {
  text-align: center;
}

.label-zone-right > .label-fixed-text {
  text-align: right;
}

/* The logo (see applyLogo in main.js): an EMPTY div whose image comes from
   --label-logo, set once on sheetsWrapper, so the image data is never
   copied into thousands of cells or into saved hand edits. Its height is a
   fraction (--label-logo-scale: Small/Medium/Large) of the label's
   available content height (--content-h, set per sheet from the template),
   and its width follows the image's aspect ratio, capped at its zone's
   width; background-size: contain keeps the image undistorted when the cap
   bites. Backgrounds don't print by default, hence print-color-adjust,
   the same as .label-cell::before. With no logo set, the scale is 0 and
   the box has no size at all. */
.label-logo {
  flex-shrink: 0;
  height: calc(var(--content-h, 0in) * var(--label-logo-scale, 0));
  aspect-ratio: var(--label-logo-aspect, 1);
  max-width: 100%;
  background-image: var(--label-logo, none);
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  print-color-adjust: exact;
  -webkit-print-color-adjust: exact;
}

/* Pre-zones markup. Fresh renders no longer produce it, but a hand-edited
   cell saves its raw HTML (cellEdits), so projects and autosaves from
   before zones existed still carry it and must keep rendering exactly as
   they did. Don't delete these rules. */
.label-cell-wrapper {
  display: flex;
  width: 100%;
  height: 100%;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
}

.label-text-side {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
}

.label-text-side > .barcode-1d-svg {
  margin-top: 2px;
}

.barcode-1d-svg {
  display: block;
}

/* Sized from the template's own geometry (see --qr-size in
   applyTemplateGeometry, main.js) rather than a fixed 0.6in: the smaller of
   0.6in and the label's available height, so a QR is never asked to be
   taller than the label itself (5167 is 0.5in tall — a fixed 0.6in QR
   couldn't fit regardless of where it sat). Always in flow inside its zone
   (see .label-zones above), never absolutely positioned — see
   docs/current-state.md, Barcode generation and QR code generation, for
   why that in-flow requirement is also what makes the barcode's own width
   measurement and the overflow check trustworthy. */
.qr-code-box {
  width: var(--qr-size, 0.6in);
  height: var(--qr-size, 0.6in);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* No image-rendering: pixelated here (deliberately, not an oversight): the
   canvas is now rendered several times larger than this box (see
   QR_RENDER_SCALE, main.js) and scaled back DOWN to fit, so the browser's
   default smooth downscaling looks better than forcing nearest-neighbor —
   pixelated was only ever compensating for the old fixed 128px canvas
   being scaled UP to a comparable on-screen size. */
.qr-code-box canvas,
.qr-code-box img {
  width: 100% !important;
  height: 100% !important;
}

/* Color Accent Bars & Shapes */
.label-color-bar {
  position: absolute;
  transition: all 0.15s ease;
}

/* Shape Variations */
.shape-bar .label-color-bar {
  left: 0.08in;
  top: 0.1in;
  bottom: 0.1in;
  width: 5px;
  height: auto;
  border-radius: 3px;
}

.shape-dot .label-color-bar {
  left: 0.08in;
  top: 0.08in;
  width: 12px;
  height: 12px;
  border-radius: 50%;
}

.shape-dot-square .label-color-bar {
  left: 0.08in;
  top: 0.08in;
  width: 12px;
  height: 12px;
  border-radius: 3px;
}

.shape-dot-diamond .label-color-bar {
  left: 0.08in;
  top: 0.08in;
  width: 12px;
  height: 12px;
  transform: rotate(45deg);
}

.shape-dot-star .label-color-bar {
  right: 0.08in;
  top: 25%;
  width: 40px;
  height: 40px;
  clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
}

.shape-dot-flower .label-color-bar {
  right: 0.08in;
  top: 25%;
  width: 40px;
  height: 40px;
  background-color: currentColor; 
  clip-path: polygon(
    45% 15%, 50% 0%, 55% 15%, 58% 30%, 
    70% 12%, 85% 15%, 88% 30%, 70% 42%, 
    85% 45%, 100% 50%, 85% 55%, 70% 58%, 
    88% 70%, 85% 85%, 70% 88%, 58% 70%, 
    55% 85%, 50% 100%, 45% 85%, 42% 70%, 
    30% 88%, 15% 85%, 12% 70%, 30% 58%, 
    15% 55%, 0% 50%, 15% 45%, 30% 42%, 
    12% 30%, 15% 15%, 30% 12%, 42% 30%
  );
}

.shape-dot-ribbon .label-color-bar {
  right: 0.08in;
  top: 25%;
  width: 40px;
  height: 40px;
  background-color: currentColor;
  background-image: repeating-linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.25) 0px,
    rgba(255, 255, 255, 0.25) 2px,
    transparent 2px,
    transparent 6px
  );
  clip-path: polygon(0% 0%, 100% 0%, 85% 50%, 100% 100%, 0% 100%);
}

.shape-dot-starburst .label-color-bar {
  right: 0.08in;
  top: 25%;
  width: 40px;
  height: 40px;
  background-color: currentColor;
  background-image: repeating-linear-gradient(
    45deg,
    rgba(0, 0, 0, 0.15),
    rgba(0, 0, 0, 0.15) 3px,
    transparent 3px,
    transparent 6px
  );
  clip-path: polygon(50% 0%, 65% 15%, 85% 15%, 85% 35%, 100% 50%, 85% 65%, 85% 85%, 65% 85%, 50% 100%, 35% 85%, 15% 85%, 15% 65%, 0% 50%, 15% 35%, 15% 15%, 35% 15%);
}

.shape-dot-badge .label-color-bar {
  right: 0.08in;
  top: 25%;
  width: 50px;
  height: 65px;
  background-color: currentColor;
  background-image: repeating-linear-gradient(
    90deg,
    transparent,
    rgba(0, 0, 0, 0.15) 3px,
    transparent 3px,
    transparent 6px
  );
  clip-path: polygon(50% 0%, 58% 42%, 95% 75%, 50% 55%, 5% 75%, 42% 42%);
}

.shape-dot-triangle .label-color-bar {
  left: 0.08in;
  top: 0.08in;
  width: 14px;
  height: 14px;
  clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}

.shape-dot-hexagon .label-color-bar {
  left: 0.08in;
  top: 0.08in;
  width: 14px;
  height: 14px;
  clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
}

.shape-top-banner .label-color-bar {
  left: 0.08in;
  right: 0.08in;
  top: 0.1in;
  bottom: 0.1in;
  height: 50%;
  width: auto;
  background-color: currentColor;
  clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 90% 100%, 70% 10%, 30% 10%, 10% 100%, 0% 100%);
}

.shape-top-umbrella .label-color-bar {
  left: 0.08in;
  right: 0.08in;
  top: 0.1in;
  bottom: 0.1in;
  height: 18px;
  width: auto;
  background-color: currentColor;
  background-image: repeating-linear-gradient(
    45deg,
    rgba(255, 255, 255, 0.25) 0px,
    rgba(255, 255, 255, 0.25) 2px,
    transparent 2px,
    transparent 6px
  );
  clip-path: polygon(0% 100%, 50% 0%, 30% 100%, 70% 100%, 50% 0%, 100% 100%, 80% 100%, 20% 100%);
}

.shape-full-stripe .label-color-bar {
  left: 0;
  top: 0;
  bottom: 0;
  width: 7px;
  height: 100%;
  border-radius: 0;
}

.arrow-left-to-right .label-color-bar {
  width: 30px;
  height: 40px;
  background-color: currentColor;
  clip-path: polygon(0% 0%, 40% 50%, 0% 100%, 60% 100%, 100% 50%, 60% 0%);
}

/* Color Legend Panel */
.color-legend {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: var(--parchment);
  border: 2px dashed var(--gold);
  border-radius: 14px;
  margin-top: 6px;
}

/* Shown once per render, not per cell, when any row's barcode couldn't be
   encoded as CODE128 or its QR code wouldn't fit at a scannable size (see
   updateBarcodeEncodingNotice / updateQrSizingNotice in main.js) — a
   bordered callout in the same spirit as .color-legend above, tinted with
   --barcode-warning rather than --gold so it reads as a warning, not a
   neutral panel. Both use this one class; each has its own element/id so
   the two problems are never merged into one confusing sentence. */
.render-issue-notice {
  font-family: var(--font-ui);
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--ink);
  padding: 10px 12px;
  background: color-mix(in srgb, var(--barcode-warning) 12%, var(--paper));
  border: 2px solid var(--barcode-warning);
  border-radius: 14px;
  margin-top: 6px;
}

.legend-header {
  font-family: var(--font-ui);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.78rem;
  font-weight: 800;
  color: var(--banner-deep);
}

.legend-items-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.cycle-style-btn {
  /* Exception to the button rule above: it's a <button>, so it'd inherit
     Fredoka otherwise, but at 0.72rem Fredoka gets hard to read this small.
     Stays on Atkinson, and its 800 weight stays too — Atkinson goes up to
     800, unlike Fredoka's 700 ceiling. */
  font-family: var(--font-ui);
  padding: 4px 10px;
  font-size: 0.72rem;
  background: var(--paper);
  color: var(--flag);
  border: 2px solid var(--flag);
  border-radius: 8px;
  cursor: pointer;
  box-shadow: 0 2px 0 var(--flag);
  font-weight: 800;
}

.cycle-style-btn:hover {
  background: var(--flag);
  color: var(--paper);
  transform: translateY(-1px);
}

.legend-item {
  font-family: var(--font-ui); /* legend values come straight from the user's CSV, same reasoning as the mapping selects */
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--ink);
  background: var(--paper);
  padding: 5px 10px;
  border-radius: 12px;
  border: 1.5px solid var(--stone-300);
  position: relative;
  cursor: pointer;
  user-select: none;
}

.legend-item:hover {
  background: var(--parchment);
}

.legend-dot {
  width: 12px;
  height: 12px;
  display: inline-block;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.1);
  transition: all 0.15s ease;
}

.shape-bar .legend-dot { border-radius: 3px; width: 5px; height: 13px; }
.shape-dot .legend-dot { border-radius: 50%; width: 12px; height: 12px; }
.shape-top-banner .legend-dot { border-radius: 2px; width: 14px; height: 4px; }
.shape-full-stripe .legend-dot { border-radius: 0; width: 7px; height: 13px; }

.legend-item.interactive {
  position: relative;
  cursor: pointer;
  user-select: none;
  transition: background-color 0.15s ease, transform 0.1s ease;
}

.legend-item.interactive:hover {
  background-color: var(--parchment);
  transform: translateY(-1px) scale(1.03);
}

.legend-color-input {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.legend-item.interactive:hover .legend-dot {
  transform: scale(1.3);
}

/* Floating Widget */
.pixel-corner-widget {
  position: fixed;
  bottom: 20px;
  right: 10px;
  z-index: 9999;
  display: block;
}

.pixel-art-img {
  width: 140px;
  height: auto;
  display: block;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  filter: drop-shadow(0 8px 12px color-mix(in srgb, var(--ink) 30%, transparent));
}

.pixel-help-bubble {
  font-family: var(--font-display);
  position: absolute;
  top: -38px;
  right: 5px;
  white-space: nowrap;
  background-color: var(--flag);
  color: var(--paper);
  font-size: 0.78rem;
  font-weight: 700; /* Fredoka's heaviest weight — don't rely on the browser clamping 800 */
  padding: 6px 12px;
  border-radius: 14px;
  box-shadow: 0 4px 12px color-mix(in srgb, var(--flag) 30%, transparent);
  cursor: text;
  user-select: text;
  animation: float-bounce 1.8s infinite ease-in-out;
}

/* Hold still under the pointer so the address is easy to select. */
.pixel-help-bubble:hover {
  animation-play-state: paused;
}

/* One click selects the whole address, ready to copy into webmail. */
.pixel-help-bubble .support-address {
  user-select: all;
}

.pixel-help-bubble::after {
  content: "";
  position: absolute;
  bottom: -5px;
  right: 24px;
  width: 0;
  height: 0;
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-top: 6px solid var(--flag);
}

@keyframes float-bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-5px); }
}

@media (max-width: 600px) {
  .pixel-corner-widget {
    bottom: 12px;
    right: 12px;
  }
  
  .pixel-art-img {
    width: 100px;
    height: auto;
  }
}

/* =========================================================
   Unified Print Stylesheet
   ========================================================= */
@media print {
  @page {
    size: 8.5in 11in;
    margin: 0;
  }

  body {
    background: none;
  }

  /* .top-bar and .controls-footer sit inside .controls-panel already;
     listed anyway so moving either out of it can't bring it onto paper. */
  .controls-panel,
  .top-bar,
  .controls-footer,
  .pixel-corner-widget,
  .render-progress-overlay,
  .drop-overlay {
    display: none !important;
  }

  .sheet-container {
    margin-left: 0;
    padding: 0;
    background-image: none;
  }

  .sheets-wrapper {
    gap: 0;
  }

  .label-sheet {
    box-shadow: none;
    border-radius: 0;
  }

  .label-cell {
    border: none !important;
    background-color: transparent;
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
  }

  .label-color-bar {
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
  }

  .label-cell.content-overflow {
    outline: none;
  }

  .label-cell.content-overflow::after {
    display: none;
  }

  .label-cell.barcode-unencodable {
    outline: none;
  }

  .label-cell.barcode-unencodable::after {
    display: none;
  }
}