/* snow symbol font */
@font-face {
  font-family: 'snowsymbolsiacs';
  src: url('../assets/fonts/SnowSymbolIACS.woff2') format('woff2'),
       url('../assets/fonts/snowsymbolsiacs.woff')  format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

.snow-symbol {
  font-family: 'snowsymbolsiacs', sans-serif;
  font-size: 1.2em;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* tokens */
:root {
  /* Palette of the host (avalanche.report, albina-website). */
  --color-bg: #f2f7fa;
  --color-surface: #ffffff;
  --color-surface-alt: #e6eef2;
  --color-border: #dae2e6;
  --color-border-focus: #19abff;
  --color-text: #565f61;
  --color-text-muted: #839194;
  --color-primary: #19abff;
  --color-primary-hover: #1489cc;
  --color-primary-text: #ffffff;
  --color-danger: #ff0000;
  --color-success: #16a34a;
  --color-warning: #ff9900;
  --color-warning-text: #a35f00;
  --color-accent: #7c3aed;
  --color-focus-ring: rgba(25, 171, 255, 0.2);
  --color-row-hover: rgba(25, 171, 255, 0.06);
  --color-row-alt: rgba(0, 0, 0, 0.015);
  --color-tab-active: var(--color-primary);
  --color-readonly: #f2f7fa;

  /* The host design uses square corners throughout. */
  --radius-sm: 0;
  --radius-md: 0;
  --radius-lg: 0;

  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-md: 0 2px 8px rgba(0,0,0,0.08);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
  --shadow-xl: 0 16px 48px rgba(0,0,0,0.16);

  /* Match the host (avalanche.report) system-font. */
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
    Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Arial,
    sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
  --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;

  /* Type scale (absolute px — the 14px html base makes rem misleading). Floor
   * of readable UI text is 13px; 16px inputs match the touch/iOS requirement.
   *   2xs 11  micro (map attribution, grain codes)
   *   xs  12  table headers, dense eyebrows
   *   sm  13  table cells, hints, secondary/helper text
   *   base14  default UI: labels, tabs, buttons, options, body
   *   md  16  input values, page/section emphasis
   *   lg  18  h2, dialog titles
   *   xl  20  h1 */
  --text-2xs: 11px;
  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-md: 16px;
  --text-lg: 18px;
  --text-xl: 20px;

  --transition: 150ms ease;
  --transition-slow: 300ms ease;
}

/* dark mode */
[data-theme="dark"] {
  --color-bg: #0f1419;
  --color-surface: #1a2332;
  --color-surface-alt: #1e2a3a;
  --color-border: #2d3a4a;
  --color-border-focus: #19abff;
  --color-text: #e4e8ee;
  --color-text-muted: #8899aa;
  --color-primary: #19abff;
  --color-primary-hover: #66c7ff;
  --color-focus-ring: rgba(25, 171, 255, 0.3);
  --color-row-hover: rgba(25, 171, 255, 0.08);
  --color-row-alt: rgba(255, 255, 255, 0.02);
  --color-readonly: #1e2a3a;
  --color-warning-text: #ffb84d;
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
  --shadow-md: 0 2px 8px rgba(0,0,0,0.3);
  --shadow-lg: 0 8px 24px rgba(0,0,0,0.4);
}

/* reset */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.5;
  color: var(--color-text);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  /* `clip` instead of `hidden`: same visual (no horizontal scrollbar
   * from rogue wide elements) but `hidden` establishes a containing
   * block which traps `position: sticky` inside the viewport instead
   * of letting it stick relative to scroll position. Especially nasty
   * in Safari. `clip` has none of those side effects. Supported in
   * Chrome 90+ / Firefox 81+ / Safari 16+. */
  overflow-x: clip;
  /* Hide the page scrollbar entirely. Scroll still works (wheel,
   * trackpad, keyboard, drag) — the visible track + thumb on the
   * right edge are just suppressed. Pair with the sticky tab bar so
   * users always know they CAN scroll without needing the chrome. */
  scrollbar-width: none;            /* Firefox */
}
html::-webkit-scrollbar {           /* Chrome / Safari / Edge */
  width: 0;
  height: 0;
  display: none;
}

body {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  overflow-x: clip;
}
/* Prevents a flash of English text while the active language's dictionary
   is being fetched. The class is added synchronously by the inline script
   in <head> and removed by app.js once applyI18n() has run. */
html.i18n-loading body { visibility: hidden; }

/* typography */
h1 { font-size: var(--text-xl); font-weight: 700; }
h2 { font-size: var(--text-lg); font-weight: 600; }
h3 { font-size: var(--text-md); font-weight: 600; }
/* 14px pairs with the 16px inputs — a touch smaller for hierarchy, not so small
 * it reads as a caption next to its field. */
label { font-size: var(--text-base); font-weight: 500; color: var(--color-text-muted); display: flex; align-items: center; gap: 6px; margin-bottom: 4px; min-height: 17px; }

/* container */
/* Content cap. 1600px is a comfortable wide-screen ceiling: 1080p
 * (1920) shows ~83% width with 160px of side breathing room; ultra-wide
 * (2560+) still feels framed rather than empty. Bump higher only if
 * the form layout starts to feel sparse — the tab tables don't gain
 * much past this point. */
.container { max-width: 1600px; margin: 0 auto; padding: 0 16px; }

/* header */
.app-header {
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  position: sticky;
  top: 0;
  z-index: 100;
}

/* Header height as a CSS variable so other sticky elements (e.g. the
 * tab bar) can stack BELOW the header without hardcoding the number. */
:root { --header-h: 56px; }

.header-inner {
  max-width: 1600px;     /* matches .container so header + body align */
  margin: 0 auto;
  padding: 0 16px;
  height: var(--header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

/* Header lead: identity (brand) + a divider + the contextual page title. */
.header-lead {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;                 /* let the title truncate instead of pushing */
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-shrink: 0;
  text-decoration: none;
  color: #66c7ff;
}
.brand .logo { display: block; flex-shrink: 0; }

.header-divider {
  width: 1px;
  height: 22px;
  flex-shrink: 0;
  background: var(--color-border);
}

.header-title {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
/* Medium, not bold — nothing else in the form is bold. Colour + size carry
 * the emphasis; the weight stays in step with the tabs and labels. */
.header-title__text {
  font-size: var(--text-md);
  font-weight: 500;
  color: var(--color-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The profile's location, read as a muted continuation of the title
 * ("Editing snow profile │ Innsbruck") — set off by a hairline, no chip. */
.header-place {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex-shrink: 1;
  min-width: 0;
  margin-left: 4px;
  padding-left: 12px;
  border-left: 1px solid var(--color-border);
  color: var(--color-text-muted);
}
.header-place[hidden] { display: none; }
.header-place__pin { flex-shrink: 0; opacity: 0.7; }
.header-place__text {
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.header-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  /* One line so the header keeps its fixed height (the sticky tab bar is
   * offset by it); the title truncates first. Mobile wraps to two rows. */
  flex-wrap: nowrap;
  flex-shrink: 0;
  justify-content: flex-end;
}

/* Overflow menu (⋯). On desktop, display:contents dissolves the <details>
 * wrappers so Import · Download · Preview lay out inline in the toolbar exactly
 * as before, and the ⋯ toggle is hidden. On mobile it becomes a real popover
 * (see the max-width: 768px rule further down). */
.header-overflow,
.header-overflow__menu { display: contents; }
/* Two classes so this beats `.btn { display: inline-flex }` (which the toggle
 * also carries and which is declared later in the file). */
.header-actions .header-overflow__toggle { display: none; }
.header-overflow__toggle {
  list-style: none;           /* strip the native disclosure triangle */
}
.header-overflow__toggle::-webkit-details-marker { display: none; }
.header-overflow__toggle::marker { content: ""; }

/* Mobile: the ⋯ toggle appears and the menu becomes a popover, so the toolbar
 * stays a single row ([⋯] [Upload]) that never crowds the host modal's X. */
@media (max-width: 768px) {
  .header-overflow {
    position: relative;
    display: block;
    flex: 0 0 auto;
    z-index: 150;             /* keep the open popover above the sticky tab bar */
  }
  .header-actions .header-overflow__toggle { display: inline-flex; }

  .header-overflow__menu {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    min-width: 208px;
    flex-direction: column;
    gap: 2px;
    padding: 6px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    z-index: 150;
  }
  .header-overflow[open] .header-overflow__menu { display: flex; }

  /* Menu rows: full-width, left-aligned icon + label. Labels are hidden in the
   * toolbar at this width — the !important re-shows them inside the menu. */
  .header-overflow__menu .btn {
    width: 100%;
    justify-content: flex-start;
    gap: 12px;
    min-height: 44px;
    padding-inline: 12px;     /* same icon column for .btn-icon rows */
    border: none;
    background: transparent;
    color: var(--color-text);
  }
  .header-overflow__menu .btn:hover { background: var(--color-surface-alt); }
  .header-overflow__menu .btn-text { display: inline !important; }
}

/* buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border: none;
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  font-weight: 500;
  cursor: pointer;
  transition: background var(--transition), color var(--transition),
              border-color var(--transition), transform 80ms ease, opacity 80ms ease;
  white-space: nowrap;
  font-family: inherit;
  /* <a class="btn"> would otherwise keep the UA's link underline and sit
   * apart from the real buttons next to it. */
  text-decoration: none;
  /* tap feedback */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}

.btn:active {
  transform: scale(0.94);
  opacity: 0.82;
}

.btn-primary {
  background: var(--color-primary);
  color: var(--color-primary-text);
}
.btn-primary:hover { background: var(--color-primary-hover); }

.btn-outline {
  background: transparent;
  color: var(--color-primary);
  border: 1px solid var(--color-primary);
}
.btn-outline:hover { background: var(--color-primary); color: var(--color-primary-text); }

.btn-ghost {
  background: transparent;
  color: var(--color-text-muted);
  border: 1px solid var(--color-border);
}
.btn-ghost:hover { background: var(--color-surface-alt); color: var(--color-text); }

.btn-icon {
  background: transparent;
  border: none;
  padding: 6px;
  border-radius: var(--radius-md);
  color: var(--color-text-muted);
  cursor: pointer;
  transition: background var(--transition), color var(--transition),
              transform 80ms ease, opacity 80ms ease;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}
.btn-icon:hover { background: var(--color-surface-alt); color: var(--color-text); }
.btn-icon:active { transform: scale(0.90); opacity: 0.75; }

.btn-lang {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: 600;
  letter-spacing: 0.04em;
  padding: 6px 8px;
  min-width: 36px;
}
select.btn-lang {
  display: inline-block;
  width: auto;
  height: auto;
  background-color: transparent;
  color: var(--color-text-muted);
  padding: 6px 22px 6px 8px; /* right room for the arrow */
}
select.btn-lang:hover {
  background-color: var(--color-surface-alt);
  color: var(--color-text);
}

.btn-sm {
  padding: 4px 10px;
  font-size: var(--text-sm);
}

/* layout */
.app-main { flex: 1; padding: 20px 0; }

.profile-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

/* card */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: 20px;
  box-shadow: var(--shadow-sm);
  min-width: 0;
}

/* form fields */
.field { margin-bottom: 12px; }

.meta-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 24px;
}

.meta-col {
  display: flex;
  flex-direction: column;
}

.meta-col .field-comments {
  flex: 1;
  display: flex;
  flex-direction: column;
  margin-top: 0;
}

.meta-col .field-comments textarea {
  flex: 1;
  resize: none;
  min-height: 60px;
}

@media (max-width: 640px) {
  .meta-grid { grid-template-columns: 1fr; }
}

input[type="text"],
input[type="email"],
input[type="number"],
input[type="date"],
input[type="time"],
select,
textarea {
  width: 100%;
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: inherit;
  /* 16px on every viewport. Below 16px, iOS Safari zooms the page in on focus
   * (and frequently doesn't zoom back out) — a functional break on a touch
   * form, not a cosmetic one. One size desktop + mobile also lets inputs and
   * labels read as a single type system instead of two. */
  font-size: var(--text-md);
  transition: border-color var(--transition), box-shadow var(--transition);
  outline: none;
  box-sizing: border-box;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  line-height: 36px;
}
select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 28px;
  line-height: normal;  /* override shared rule — prevents descender clipping */
  padding-top: 0;
  padding-bottom: 0;
}
input[type="date"],
input[type="time"] {
  line-height: normal;
}
textarea {
  height: auto;
  padding: 8px 10px;
  /* The shared rule above sets line-height to the 36px height of a
   * single-line field — on a multi-line comment that spaces the lines
   * a full row apart. Back to normal text leading. */
  line-height: 1.4;
}

/* hide number spinners */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible,
.coord-btn:focus-visible {
  border-color: var(--color-border-focus);
  box-shadow: 0 0 0 3px var(--color-focus-ring);
}

input.readonly, input[readonly] {
  background: var(--color-readonly);
  cursor: default;
}

select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%236b7685' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 28px;
  /* long text: … before arrow */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

textarea {
  resize: vertical;
  min-height: 80px;
}

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

/* Coordinates: the lat/lon pair plus "my position" and the map toggle. */
.coord-group {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto auto;
  gap: 8px;
}

/* Sized and bordered like an input so the four controls read as one row. */
.coord-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-muted);
  cursor: pointer;
  outline: none;
  transition: color var(--transition), border-color var(--transition),
              background var(--transition), transform 80ms ease;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

.coord-btn:hover { color: var(--color-primary); border-color: var(--color-border-focus); }
.coord-btn:active { transform: scale(0.94); }
.coord-btn:disabled { opacity: 0.35; cursor: default; }

.coord-btn[aria-expanded="true"] {
  color: var(--color-primary);
  border-color: var(--color-primary);
  background: var(--color-row-hover);
}

/* Transient geolocation feedback above the map, standing hint below it. */
.coord-status,
.coord-map-hint {
  margin-top: 6px;
  font-size: var(--text-sm);
  line-height: 1.4;
  color: var(--color-text-muted);
}
.coord-status--error { color: var(--color-danger); }

.coord-map-panel {
  margin-top: 8px;
  animation: coord-map-in var(--transition);
}

.coord-map {
  height: 250px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;   /* tiles don't respect the rounded corners on their own */
}

@keyframes coord-map-in { from { opacity: 0; } to { opacity: 1; } }

.coord-map .leaflet-container {
  font-family: inherit;
  background: var(--color-surface-alt);
}
.coord-map .leaflet-control-attribution { font-size: var(--text-2xs); }

/* Leaflet sizes the div icon's box; the pin itself just needs our accent
 * colour and room to stand above the point it marks. */
.coord-map-pin {
  color: var(--color-primary);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
}

/* Two labelled fields side by side (observation date + time), laid out like
 * the coordinate pair. The inputs are bottom-aligned so a long translation
 * that wraps its label to two lines doesn't offset its neighbour. */
.field-row { margin-bottom: 12px; }
.field-row .field {
  display: flex;
  flex-direction: column;
  margin-bottom: 0;
}
.field-row .field > :last-child { margin-top: auto; }

.input-group {
  display: flex;
  gap: 8px;
  align-items: center;
}

.input-group input { flex: 1; }

/* tabs */
.tabs-bar {
  display: flex;
  margin-bottom: 16px;
  /* Lock the tab bar just below the sticky app header so it stays
   * visible while the user scrolls through the tab panel content. */
  position: sticky;
  top: var(--header-h, 56px);
  z-index: 30;
  background-color: var(--color-surface);
  /* Tiny shadow that hints "I'm sitting on top of the content" — only
   * really visible once scrolled, which is exactly when we want it. */
  box-shadow: 0 4px 6px -4px rgba(0, 0, 0, 0.08);
}

.tabs {
  display: flex;
  flex: 1;
  min-width: 0;
  gap: 2px;
  border-bottom: 2px solid var(--color-border);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scroll-behavior: smooth;
  /* Rest on a tab edge (or the very end) so the arrow gap is the same on
   * both sides instead of wherever a page step happened to stop. */
  scroll-snap-type: x mandatory;
  /* The arrows replace the scrollbar as the overflow cue. */
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }

/* Arrows sit beside the tabs (never over them) and only while the bar
 * overflows; the one whose end is reached is dimmed rather than removed so
 * the tabs don't shift. The next arrow is the prev button mirrored. */
.tabs-arrow {
  display: none;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  padding: 0;
  border: none;
  /* Continues the tab strip's bottom rule under the arrows. */
  border-bottom: 2px solid var(--color-border);
  background: none;
  color: var(--color-text-muted);
  transition: color var(--transition), opacity var(--transition);
}
.tabs-bar.overflowing .tabs-arrow { display: flex; }
.tabs-arrow:hover:not(:disabled) { color: var(--color-tab-active); }
.tabs-arrow:disabled { cursor: default; }
.tabs-arrow:disabled svg { opacity: 0.3; }
.tabs-arrow-next { transform: scaleX(-1); }

.tab, .tabs-arrow {
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.tab:active, .tabs-arrow:active { opacity: 0.65; }

.tab {
  padding: 10px 18px;
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -2px;
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--color-text-muted);
  white-space: nowrap;
  scroll-snap-align: start;
  transition: color var(--transition), background var(--transition),
              border-color var(--transition), opacity 80ms ease;
  -webkit-user-select: none;
  user-select: none;
}

.tab:hover { color: var(--color-text); background: var(--color-row-hover); }
.tab.active {
  color: var(--color-tab-active);
  border-bottom-color: var(--color-tab-active);
}

.tab-panel { min-height: 200px; }
.tab-panel[hidden] { display: none; }

/* Mobile bottom bar: glove-sized targets, counts, hidden while typing. */
:root { --bottom-tabs-h: 60px; }
.bottom-tabs {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  padding: 0 4px env(safe-area-inset-bottom);
  background: var(--color-surface);
  border-top: 1px solid var(--color-border);
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.06);
  transition: transform var(--transition-slow);
}
body.is-typing .bottom-tabs { transform: translateY(100%); }

.bottom-tab {
  /* width follows label */
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  height: var(--bottom-tabs-h);
  min-width: 0;
  padding: 8px 2px;
  border: none;
  background: none;
  color: var(--color-text-muted);
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: 600;
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  -webkit-user-select: none;
  user-select: none;
}
.bottom-tab__pill {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 40px;
  padding: 0 6px;
  border-radius: 0;
  transition: background var(--transition), transform 80ms ease;
}
.bottom-tab:active .bottom-tab__pill { transform: scale(0.94); }
.bottom-tab.active { color: var(--color-primary); }
.bottom-tab.active .bottom-tab__pill {
  background: color-mix(in srgb, var(--color-primary) 18%, transparent);
}
.bottom-tab__label {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bottom-tab__badge {
  position: absolute;
  top: -6px;
  right: -2px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--color-primary);
  color: #fff;
  font-size: var(--text-2xs);
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--color-surface);
}
.bottom-tab__badge[hidden] { display: none; }
@media (max-width: 360px) {
  .bottom-tab { padding-inline: 0; font-size: var(--text-sm); }
  .bottom-tab__pill { padding: 0 3px; }
}
/* meta: required fields missing, as on the field labels */
.bottom-tab__required {
  margin-left: 1px;
  color: var(--color-danger);
  font-weight: 700;
}
.bottom-tab__required[hidden] { display: none; }

/* data tables */
.data-table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.data-table th {
  text-align: left;
  padding: 8px 10px;
  font-weight: 600;
  font-size: var(--text-xs);
  letter-spacing: 0.01em;
  color: var(--color-text-muted);
  border-bottom: 2px solid var(--color-border);
  white-space: nowrap;
  background: var(--color-surface);
  position: sticky;
  top: 0;
}

.data-table td {
  padding: 7px 10px;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}

.data-table tbody tr:nth-child(even) { background: var(--color-row-alt); }
.data-table tbody tr:hover { background: var(--color-row-hover); }

.stability-test-table > tbody > .stability-test-parent td {
  background: var(--color-surface-alt);
  font-weight: 600;
}

.stability-test-table > tbody > .stability-test-parent {
  cursor: pointer;
}

.stability-test-table .stability-test-heading {
  white-space: nowrap;
}

.stability-test-add .stability-test-class-field {
  flex: 2;
}

.stability-test-class-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.stability-test-expand {
  margin-right: 8px;
  padding: 2px 4px;
  border: 0;
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  font: inherit;
}

.stability-test-results-row > td {
  padding: 8px 12px 12px 28px;
  background: var(--color-surface);
}

.stability-test-results-table {
  width: 100%;
  table-layout: fixed;
  border: 1px solid var(--color-border);
}

.stability-test-results-table th,
.stability-test-results-table td {
  overflow-wrap: anywhere;
}

.stability-test-results-table .stability-test-result .result-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 2px;
}

.stability-test-results-table th {
  position: static;
}

.stability-test-results-table .empty-state {
  text-align: left;
  color: var(--color-text-muted);
}

.stability-test-results-table .stability-test-result-editor select,
.stability-test-results-table .stability-test-result-editor input {
  display: block;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
  padding: 5px 7px;
}

/* Locked by a result rule (fixed value, or field does not apply). */
.stability-test-results-table .stability-test-result-editor select:disabled,
.stability-test-results-table .stability-test-result-editor input:disabled {
  background: var(--color-readonly);
  color: var(--color-text-muted);
  cursor: not-allowed;
}

.stability-test-results-table .stability-test-result-editor .entry-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 4px;
}

.stability-test-results-table .stability-test-result-editor td {
  min-width: 0;
}

.stability-test-results-table .stability-test-result-editor .actions {
  width: auto;
  white-space: normal;
}

.stability-test-results-table .stability-test-result-editor .btn {
  padding: 5px 8px;
  font-size: var(--text-xs);
}

.stability-test-add-result-icon {
  display: block;
}

.stability-test-results-table th.required::after {
  content: "*";
  margin-left: 2px;
  color: var(--color-danger);
  font-weight: 700;
}

.stability-test-results-table .stability-test-result-editor.show-required select:invalid,
.stability-test-results-table .stability-test-result-editor.show-required input:invalid {
  border-color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 6%, transparent);
}

/* Height off every layer border: a hint, not an error — the row can still
 * be saved. An error frame (:invalid) wins over it. */
.stability-test-results-table .stability-test-result-editor input.field-warning:valid {
  border-color: var(--color-warning);
  background: color-mix(in srgb, var(--color-warning) 12%, transparent);
}
.stability-test-results-table .stability-test-result-editor input.field-warning:valid:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-warning) 25%, transparent);
}
.field-warning-msg {
  color: var(--color-warning-text);
  font-size: 0.6875rem;
  line-height: 1.3;
  margin-top: 3px;
}

.data-table .actions {
  white-space: nowrap;
  text-align: right;
}

.data-table .actions button {
  background: none;
  border: none;
  padding: 4px 6px;
  cursor: pointer;
  color: var(--color-text-muted);
  border-radius: var(--radius-sm);
  transition: all var(--transition);
  font-family: inherit;
}

.data-table .actions button:hover { color: var(--color-text); background: var(--color-surface-alt); }
.data-table .actions button.delete:hover { color: var(--color-danger); }

/* entry form */
.entry-form {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  margin-bottom: 12px;
  align-items: flex-end;
}

.entry-form .field {
  margin-bottom: 0;
  min-width: 80px;
  flex: 1;
}

.entry-form .field label {
  font-size: var(--text-sm);
  margin-bottom: 2px;
}

.entry-form input,
.entry-form select {
  padding: 6px 8px;
  font-size: var(--text-md);
}
.entry-form select { padding-right: 28px; }

.form-error {
  grid-column: 1 / -1;
  color: #e05252;
  font-size: var(--text-sm);
  padding: 2px 0;
}

/* Required header fields: an asterisk on the caption, and a red frame on the
 * field itself once an upload attempt found it empty (see checkRequiredMeta).
 * `label` is a flex container, so the marker lands after the caption text
 * with the label's own 6px gap — pulled back in a little. */
.field:has(:required) > label::after {
  content: "*";
  margin-left: -3px;
  color: var(--color-danger);
  font-weight: 700;
}

.required-hint {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  margin-bottom: 12px;
}

/* Empty required fields are flagged only after an upload attempt found
 * them empty (app.js adds .show-required) — never on a form the user has
 * only just opened. From then on :invalid does the bookkeeping itself:
 * the frame disappears the moment the field is filled in. */
#panel-meta.show-required :required:invalid {
  border-color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 6%, transparent);
}
#panel-meta.show-required :required:invalid:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger) 20%, transparent);
}

/* Numeric fields validate themselves: dom.js gives them a `pattern`, the ones
 * a row cannot do without are `required`, and field-validation.js feeds the
 * min/max range back through setCustomValidity(). A letter turns the frame
 * red on the keystroke that types it; :placeholder-shown holds an empty field
 * back, so a field merely left blank is flagged by the .show-required staging
 * — after an Add attempt, not before. */
.entry-form input:invalid:not(:placeholder-shown),
.entry-form.show-required input:invalid,
#panel-meta input[pattern]:invalid:not(:placeholder-shown) {
  border-color: var(--color-danger);
  background: color-mix(in srgb, var(--color-danger) 6%, transparent);
  color: var(--color-danger);
}
.entry-form input:invalid:not(:placeholder-shown):focus,
.entry-form.show-required input:invalid:focus,
#panel-meta input[pattern]:invalid:not(:placeholder-shown):focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-danger) 20%, transparent);
}

.field-error {
  color: var(--color-danger);
  font-size: 0.75rem;
  line-height: 1.3;
  margin-top: 3px;
}
.entry-form .field-error {
  font-size: 0.6875rem;
}
.field-hint {
  color: var(--color-text-muted);
  font-size: 0.75rem;
  line-height: 1.3;
  margin-top: 3px;
}

/* Add/Update stays out of reach while a field holds something unusable. */
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.entry-form-title {
  flex: 0 0 100%;
  width: 100%;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text-muted);
  padding-bottom: 2px;
  border-bottom: 1px solid var(--color-border);
  margin-bottom: 2px;
}

.entry-form .entry-actions {
  display: flex;
  gap: 6px;
  align-items: flex-end;
}

/* picker */
.picker-wrap {
  position: relative;
}

.picker-btn {
  width: 100%;
  padding: 6px 8px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
  font-family: inherit;
  font-size: var(--text-base);
  text-align: left;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  align-items: center;
  transition: border-color var(--transition);
}

.picker-btn:focus-visible {
  border-color: var(--color-border-focus);
  box-shadow: 0 0 0 3px var(--color-focus-ring);
  outline: none;
}

.picker-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 50;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  max-height: 240px;
  overflow-y: auto;
  display: none;
}

.picker-dropdown.open { display: block; }

.picker-wrap input { padding-right: 30px; }

.place-picker-btn {
  position: absolute;
  right: 4px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  padding: 4px;
  border: none;
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
}
.place-picker-btn:hover { color: var(--color-text); }

.picker-option {
  padding: 8px 10px;
  cursor: pointer;
  font-size: var(--text-base);
  transition: background var(--transition);
  display: flex;
  align-items: center;
  gap: 8px;
}

.picker-option:hover { background: var(--color-row-hover); }
.picker-option.selected { background: rgba(25, 171, 255, 0.12); font-weight: 500; }

/* radio toggle */
.toggle-group {
  display: inline-flex;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: 12px;
}

.toggle-group label {
  padding: 6px 14px;
  font-size: var(--text-base);
  font-weight: 500;
  cursor: pointer;
  color: var(--color-text-muted);
  border-right: 1px solid var(--color-border);
  transition: all var(--transition);
  margin: 0;
}

.toggle-group label:last-child { border-right: none; }
.toggle-group input[type="radio"] { display: none; }
.toggle-group input[type="radio"]:checked + label {
  background: var(--color-primary);
  color: var(--color-primary-text);
}
.toggle-group input[type="radio"]:disabled + label {
  opacity: 0.4;
  cursor: not-allowed;
}

/* parametrize */
.parametrize-wrap {
  display: flex;
  gap: 8px;
  align-items: center;
}
.parametrize-wrap[hidden] {
  display: none;
}

/* ram */
.ram-legend {
  margin: 6px 0 0;
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}
.ram-table .data-table th { white-space: normal; }
.ram-table .data-table .actions { white-space: nowrap; }
.ram-row--start td { background: var(--color-surface-alt); }
.data-table .actions button:disabled { opacity: 0.3; cursor: default; }

.ram-info {
  flex: 0 0 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}
.ram-step { font-weight: 600; color: var(--color-primary); }
.ram-info--warn { color: var(--color-danger); }
.ram-step[hidden] { display: none; }
.ram-entry .field:has(.ram-stepper) { min-width: 140px; }
.ram-entry input:disabled {
  background: var(--color-surface);
  color: var(--color-text-muted);
  opacity: 0.6;
  cursor: not-allowed;
}
.ram-stepper { display: flex; gap: 4px; }
.ram-stepper input { flex: 1; min-width: 0; text-align: center; }
.ram-stepper .btn {
  flex: none;
  width: 36px;
  padding: 0;
  justify-content: center;
  font-size: var(--text-md);
  border: 1px solid var(--color-border);
}
@media (max-width: 768px) {
  .ram-table .data-table .actions button { padding-inline: 6px; }
  /* column form: full width, natural height */
  .entry-form.ram-entry { align-items: stretch; }
  .ram-entry > * { flex: none; }
  .ram-entry .entry-actions { flex-wrap: wrap; justify-content: flex-end; }
}

/* collapsible weather */
.weather-details {
  margin-bottom: 12px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  padding: 0;
}
.weather-details summary {
  padding: 8px 12px;
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--color-text-muted);
  cursor: pointer;
  user-select: none;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
.weather-details summary::-webkit-details-marker { display: none; }
.weather-details summary::before {
  content: '';
  display: inline-block;
  width: 0; height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.15s ease;
}
.weather-details[open] summary::before {
  transform: rotate(90deg);
}
.weather-details > .meta-grid {
  padding: 4px 12px 8px;
}

/* comments field */
.field-comments {
  margin-top: 4px;
}
.field-comments textarea {
  min-height: 60px;
  resize: vertical;
}

.dialog-header {
  padding: 16px 20px;
  border-bottom: 1px solid var(--color-border);
}

.dialog-header h2 { margin: 0; }
.dialog-header .hint { color: var(--color-text-muted); font-size: var(--text-sm); margin-top: 2px; }

.dialog-footer {
  padding: 12px 20px;
  border-top: 1px solid var(--color-border);
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

/* confirm dialog */
#confirm-dialog {
  border: none;
  border-radius: var(--radius-lg);
  padding: 0;
  width: min(90vw, 400px);
  box-shadow: var(--shadow-xl);
  background: var(--color-surface);
  color: var(--color-text);
}

#confirm-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}

#confirm-dialog p { padding: 16px 20px; }

/* preview dialog */
.preview-dialog {
  border: none;
  border-radius: 0;
  padding: 0;
  width: 100vw;
  height: 100vh;
  max-width: 100vw;
  max-height: 100vh;
  box-shadow: none;
  background: var(--color-surface);
  color: var(--color-text);
  margin: 0;
  inset: 0;
  overflow: hidden;
}

.preview-dialog[open] {
  display: flex;
  flex-direction: column;
}

.preview-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
}

.preview-dialog .dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
/* Medium, not bold — matches .header-title__text; nothing else in the
 * form is bold, so the preview dialog shouldn't read heavier than it. */
.preview-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 500;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.preview-body {
  padding: 12px 20px;
  overflow: auto;
  flex: 1;
  min-height: 0;
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

.preview-body svg {
  border: 1px solid #ccc;
  width: 100%;
  height: calc(100vh - 90px);
  background: #fff;
  transform-origin: top center;
}

/* Floating +/- zoom control, docked beside the plot in the dialog's own
 * side margin rather than over it. A sibling of .preview-body (not a
 * child), fixed to the dialog edge — position:fixed inside an .preview-body
 * that later grows via `transform: scale()` would otherwise be treated as
 * part of its scrollable content and scroll out of view. */
.preview-zoom-controls {
  position: fixed;
  right: 20px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 6px;
  border-radius: 999px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  box-shadow: var(--shadow-md);
}

.preview-zoom-btn {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--color-text);
  font-size: var(--text-md);
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.preview-zoom-btn:hover:not(:disabled) { background: var(--color-surface-alt); }
.preview-zoom-btn:disabled { opacity: 0.35; cursor: default; }

.preview-zoom-level {
  min-width: 42px;
  text-align: center;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
}

/* toggled toolbar button, e.g. select mode */
.preview-dialog .header-actions .btn.active {
  background: var(--color-primary);
  color: var(--color-primary-text);
}

/* .btn/.preview-zoom-controls force display: override needed to hide them */
.preview-dialog .header-actions .btn[hidden] { display: none; }
.preview-zoom-controls[hidden] { display: none; }

/* interactive layer select/delete column */
.layer-selector {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  max-width: 380px;
}
.layer-selector[hidden] { display: none; }

.layer-selector-hint {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  text-align: center;
}

/* sized via width/height attrs in JS; .layer-selector prefix beats .preview-body svg specificity */
.layer-selector .layer-selector-svg {
  max-width: 100%;
  width: auto;
  height: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.layer-selector-rect {
  fill: var(--grain-fill, var(--color-surface-alt));
  cursor: pointer;
}
.layer-selector-rect:hover { stroke: var(--color-primary); stroke-width: 2; }
/* dark blue: avoids clashing with SH (red) / DH (bright blue) grain colors */
.layer-selector-rect.selected {
  fill: #1e3a8a;
  fill-opacity: 0.45;
  stroke: #1e3a8a;
  stroke-width: 2;
}

/* fixed width: label length varies with selection count */
#layer-selector-delete {
  width: 220px;
  justify-content: center;
}

.btn-danger {
  background: var(--color-danger);
  color: #fff;
  border: 1px solid var(--color-danger);
}
.btn-danger:hover:not(:disabled) { background: #cc0000; }

/* empty state */
.empty-state {
  text-align: center;
  padding: 32px 16px;
  color: var(--color-text-muted);
}

.empty-state svg {
  margin-bottom: 8px;
  opacity: 0.4;
}

.empty-state p { font-size: var(--text-base); }

/* scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 0; }
::-webkit-scrollbar-thumb:hover { background: var(--color-text-muted); }

/* mobile */
@media (max-width: 768px) {
  .btn-desktop-only { display: none !important; }

  /* app bar: one row */
  .header-inner { padding: 0 8px 0 12px; gap: 4px; }
  .header-lead { flex: 1 1 auto; gap: 10px; }
  .header-divider { display: none; }
  .brand .logo { display: block; width: auto; height: 28px; }
  .header-title { gap: 8px; }
  .header-title__text { font-size: var(--text-md); }
  .header-place {
    margin-left: 0;
    padding-left: 0;
    border-left: none;
    flex-shrink: 100;   /* place yields first */
    overflow: hidden;
  }

  .header-actions { gap: 2px; }
  #sel-lang { order: -1; }

  .btn { padding: 8px 12px; font-size: var(--text-base); min-height: 40px; }
  .btn-sm { padding: 6px 10px; min-height: 36px; }
  .btn-icon { padding: 8px; min-height: 40px; min-width: 40px; }
  .header-actions .btn svg { flex-shrink: 0; }

  .app-main { padding: 12px 0; }
  .container { padding: 0 10px; width: 100%; }
  .profile-grid { gap: 12px; }

  .card { padding: 12px; border-radius: var(--radius-md); }

  /* Taller controls for touch. Font sizes (16px inputs, 14px labels) come from
   * the base rules and are already consistent across viewports. */
  input[type="text"],
  input[type="email"],
  input[type="number"],
  input[type="date"],
  input[type="time"],
  select { height: 42px; line-height: normal; }
  .field { margin-bottom: 10px; }

  input, select, textarea { max-width: 100%; box-sizing: border-box; }

  /* Track the taller inputs above — the four controls share a row. */
  .coord-btn { width: 42px; height: 42px; }

  /* bottom bar replaces the strip */
  .tabs-bar { display: none; }
  body { padding-bottom: calc(var(--bottom-tabs-h) + env(safe-area-inset-bottom)); }
  .bottom-tabs { display: flex; }

  .data-table-wrap { -webkit-overflow-scrolling: touch; }
  .data-table { font-size: var(--text-sm); }
  .data-table th { padding: 6px 6px; font-size: var(--text-xs); }
  .data-table td { padding: 5px 6px; }

  .data-table .actions button {
    padding: 8px 10px;
    font-size: var(--text-md);
    min-height: 44px;
    min-width: 36px;
  }

  .stability-test-results-table .actions button {
    padding: 4px;
    min-height: 36px;
    min-width: 28px;
  }

  .stability-test-results-row > td { padding-inline: 6px; }

  .entry-form { gap: 8px; }
  .entry-form .field { min-width: 0; }

  details summary { font-size: var(--text-base); }

  .preview-body { padding: 8px; }
  .preview-body svg { height: calc(100vh - 70px); width: auto; max-width: 100%; }
  .preview-zoom-controls { right: 6px; }
}

@media (max-width: 640px) {
  /* Hides Upload's label so it collapses to its icon. Download/Preview/Scan
   * now live in the ⋯ overflow menu, which re-shows their labels itself. */
  .header-actions .btn-text { display: none; }

  #btn-save-server {
    padding: 0;
    width: 40px;
    min-height: 40px;
    margin-left: 4px;
    justify-content: center;
    border-radius: 50%;
  }
  select.btn-lang { min-height: 40px; padding-left: 6px; }

  .field-row { grid-template-columns: 1fr 1fr; gap: 6px; }

  .coord-group { gap: 6px; }
  /* The meta grid is single-column here, so the map gets the full card
   * width — worth a little more height to pick a point by thumb. */
  .coord-map { height: 240px; }

  /* layer entry grid */
  .layer-entry-form {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 8px;
    flex-wrap: unset;
    width: 100%;
  }
  .layer-entry-form .field {
    width: auto;
    min-width: 0;
    flex: unset;
  }
  .layer-entry-form .field[data-layer-group="moisture"],
  .layer-entry-form .field[data-layer-group="hardness"] {
    grid-column: 1 / -1;
  }
  .layer-entry-form .entry-actions,
  .layer-entry-form .entry-form-title {
    grid-column: 1 / -1;
  }
  .layer-entry-form select,
  .layer-entry-form input {
    width: 100%;
    max-width: 100%;
    min-width: 0;
  }

  .entry-form:not(.layer-entry-form) { flex-direction: column; }
  .entry-form:not(.layer-entry-form) .field { width: 100%; min-width: unset; }

  .meta-grid { grid-template-columns: 1fr !important; }
  .meta-col { min-width: 0; }
}

/* print */
@media print {
  .app-header, .header-actions, .entry-form, .actions, .bottom-tabs,
  .coord-map-panel { display: none !important; }
  .card { box-shadow: none; border: 1px solid #ccc; break-inside: avoid; }
  .profile-grid { grid-template-columns: 1fr; }
}


.btn-xs { font-size: var(--text-xs); padding: 2px 8px; }


.ocr-hi,
.ocr-mid,
.ocr-lo {
  transition: box-shadow 0.2s, outline 0.2s;
}
.ocr-hi  { outline: 2px solid var(--color-success) !important; outline-offset: 1px; }
.ocr-mid { outline: 2px solid #f5a623             !important; outline-offset: 1px; }
.ocr-lo  { outline: 2px solid var(--color-danger)  !important; outline-offset: 1px; }

/* new OCR rows */
.ocr-row-new {
  background: color-mix(in srgb, var(--color-primary) 6%, transparent) !important;
  border-left: 3px solid var(--color-primary);
  transition: background 0.4s, border-color 0.4s;
}
[data-theme="dark"] .ocr-row-new {
  background: color-mix(in srgb, var(--color-primary) 10%, transparent) !important;
}

/* ── Upload flow dialog ──────────────────────────────────────────── */
.upload-flow-dialog {
  /* Bulletproof centering: explicit position + inset:0 + margin:auto,
   * with `height: fit-content` re-stated so the UA's width-fit-content
   * default doesn't get clobbered by our width override and end up
   * stretching the dialog vertically.
   *
   * Tried letting the UA handle layout (no overrides) — broke in
   * Safari because once `width` is overridden, Safari sometimes drops
   * the auto-centering. Explicit recipe works in every browser. */
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(560px, 92vw);
  max-width: 560px;
  height: fit-content;
  max-height: calc(100vh - 32px);

  padding: 0;
  border: none;                /* kills the UA's default chunky solid border */
  border-radius: 0;
  background: var(--color-surface);
  color: var(--color-text, inherit);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.22);
  overflow: hidden;
}
.upload-flow-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}

/* State gating: hide every section by default, show only the active one. */
.upload-flow-dialog > section { display: none; }
.upload-flow-dialog[data-state="confirm"]   .upload-flow-confirm,
.upload-flow-dialog[data-state="uploading"] .upload-flow-uploading,
.upload-flow-dialog[data-state="success"]   .upload-flow-success,
.upload-flow-dialog[data-state="error"]     .upload-flow-error {
  display: block;
}
.upload-flow-dialog[data-state="uploading"] .upload-flow-uploading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 48px 24px;
}

/* Header / body / footer rhythm shared across states. NO internal
 * dividers — earlier we had border-bottom on the header and border-top
 * on the footer, but combined they read like an inner "frame" around
 * the body. Sections are separated by padding alone now. */
.upload-flow-dialog .dialog-header {
  padding: 22px 24px 8px;
  border: none;
}
.upload-flow-dialog .dialog-header h2,
.upload-flow-backup-heading {
  margin: 0;
  font-size: var(--text-lg);
  display: flex;
  align-items: center;
  gap: 10px;
}
.upload-flow-body {
  padding: 12px 24px 16px;
  font-size: var(--text-base);
  line-height: 1.45;
}
.upload-flow-body p { margin: 0 0 12px; }
/* One prose style across the success step: the intro and the cross-device note
 * are the same kind of text, so they shouldn't differ in colour. */
.upload-flow-success .upload-flow-body p { color: var(--color-text-muted, inherit); }
.upload-flow-dialog .dialog-footer {
  padding: 8px 24px 20px;
  border: none;
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  flex-wrap: wrap;
}


.upload-flow-summary {
  margin: 8px 0 0;
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 14px;
  row-gap: 6px;
  padding: 12px 14px;
  background: var(--color-surface-alt);
  border: 1px solid var(--color-border);
  border-radius: 0;
}
.upload-flow-summary dt {
  font-weight: 600;
  color: var(--color-text-muted, currentColor);
  text-align: right;
}
.upload-flow-summary dd {
  margin: 0;
  word-break: break-word;
}

/* Success state: link + token field group. */
.upload-flow-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
}
/* Copy button sits next to the input; input scrolls its own overflow. */
.upload-flow-copyrow {
  display: flex;
  gap: 6px;
  align-items: center;
}
.upload-flow-copyrow input {
  flex: 1;
  min-width: 0;
}
.upload-flow-copybtn {
  flex-shrink: 0;
  padding: 5px;
}
.upload-flow-copybtn:active {
  transform: scale(0.9);
}
.upload-flow-copybtn .icon-copied { display: none; }
.upload-flow-copybtn.is-copied .icon-copy { display: none; }
.upload-flow-copybtn.is-copied .icon-copied {
  display: block;
  color: var(--color-success);
}
/* Labels inherit the app-wide `label` rule; only the doubled gap needs undoing. */
.upload-flow-field label {
  margin-bottom: 0;
}
/* Monospace is the one deliberate second family here: these values are random
 * base64url, where l/1/I and O/0 have to stay distinguishable. Size tracks the
 * body text — inputs don't inherit it on their own. */
.upload-flow-field input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: inherit;
  padding: 8px 10px;
  border: 1px solid var(--color-border);
  border-radius: 0;
  background: var(--color-bg);
  color: inherit;
}
/* "Editing on another device?" block — separated by space alone, in keeping
 * with the dialog's no-internal-dividers rule above. */
.upload-flow-backup {
  margin: 22px 0 4px;
}
.upload-flow-backup-heading {
  font-size: var(--text-md);
}
.upload-flow-backup-note {
  margin: 8px 0 12px !important;
}

/* ── Edit-token prompt — .upload-flow-dialog, narrowed: one short field reads
 * lost in the shared 560px shell. */
.edit-token-dialog {
  width: min(420px, 92vw);
  max-width: 420px;
}
.edit-token-dialog .upload-flow-field input {
  width: 100%;
  box-sizing: border-box;
}
.edit-token-error {
  margin: 0 0 10px !important;
  font-size: var(--text-sm);
  color: var(--color-danger, #dc2626);
}
.edit-token-note {
  margin: 0 !important;
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--color-text-muted, inherit);
}

.upload-flow-success-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding-top: 28px;
  padding-bottom: 18px;
  border-bottom: none;
}
.upload-flow-success-header h2 {
  margin: 0;
  font-weight: 600;
  text-align: center;
}
.upload-flow-checkmark {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  display: block;
  stroke-width: 3;
  stroke: var(--color-primary);
  stroke-miterlimit: 10;
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 35%, transparent);
  animation:
    upload-flow-fill 0.4s ease-in-out 0.4s forwards,
    upload-flow-scale 0.3s ease-in-out 0.9s both;
}
.upload-flow-checkmark-circle {
  stroke-dasharray: 166;
  stroke-dashoffset: 166;
  stroke-width: 3;
  stroke: var(--color-primary);
  fill: none;
  animation: upload-flow-circle 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.upload-flow-checkmark-check {
  stroke-dasharray: 48;
  stroke-dashoffset: 48;
  stroke: #fff;
  stroke-width: 4;
  stroke-linecap: round;
  stroke-linejoin: round;
  transform-origin: 50% 50%;
  animation: upload-flow-check 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}

@keyframes upload-flow-circle {
  to { stroke-dashoffset: 0; }
}
@keyframes upload-flow-fill {
  to {
    box-shadow:
      inset 0 0 0 36px var(--color-primary),
      0 0 0 8px color-mix(in srgb, var(--color-primary) 18%, transparent);
  }
}
@keyframes upload-flow-check {
  to { stroke-dashoffset: 0; }
}
@keyframes upload-flow-scale {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}


.upload-flow-checkmark-err {
  stroke: var(--color-danger, #dc2626);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-danger, #dc2626) 35%, transparent);
  animation:
    upload-flow-fill-err 0.4s ease-in-out 0.4s forwards,
    upload-flow-scale    0.3s ease-in-out 0.9s both;
}
.upload-flow-checkmark-err .upload-flow-checkmark-circle {
  stroke: var(--color-danger, #dc2626);
}
.upload-flow-checkmark-err .upload-flow-checkmark-check {
  stroke-dasharray: 52;
  stroke-dashoffset: 52;
}
@keyframes upload-flow-fill-err {
  to {
    box-shadow:
      inset 0 0 0 36px var(--color-danger, #dc2626),
      0 0 0 8px color-mix(in srgb, var(--color-danger, #dc2626) 18%, transparent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .upload-flow-checkmark,
  .upload-flow-checkmark-circle,
  .upload-flow-checkmark-check {
    animation: none;
  }
  .upload-flow-checkmark {
    box-shadow: inset 0 0 0 36px var(--color-primary);
  }
  .upload-flow-checkmark-err {
    box-shadow: inset 0 0 0 36px var(--color-danger, #dc2626);
  }
  .upload-flow-checkmark-circle { stroke-dashoffset: 0; }
  .upload-flow-checkmark-check  { stroke-dashoffset: 0; }
}

.upload-flow-error-msg {
  padding: 12px 14px;
  background: var(--color-surface-alt);
  color: var(--color-text-muted, inherit);
  border-radius: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  white-space: pre-wrap;
  word-break: break-word;
}

.upload-flow-spinner {
  width: 40px;
  height: 40px;
  border: 3px solid var(--color-border);
  border-top-color: var(--color-primary);
  border-radius: 50%;
  animation: upload-flow-spin 0.9s linear infinite;
}
.upload-flow-status {
  margin: 0;
  font-size: var(--text-base);
  color: var(--color-text-muted, inherit);
}
@keyframes upload-flow-spin {
  to { transform: rotate(360deg); }
}

.mobile-grain-picker {
  position: fixed;
  /* The UA popover style sets inset: 0; with margin: 0 below, top: 0 would
   * win and pin the strip to the top (under the host's close button). */
  top: auto;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100%;
  max-width: 100vw;
  margin: 0;
  padding: 0;
  border: none;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
  box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.12);
  /* env(safe-area-inset-bottom) keeps clear of the iPhone home indicator */
  padding-bottom: env(safe-area-inset-bottom);
}
.mobile-grain-picker:popover-open {
  display: flex;
  align-items: stretch;
}
.mobile-grain-picker-scroll {
  flex: 1 1 auto;
  display: flex;
  gap: 8px;
  padding: 10px 8px;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  scrollbar-width: none;            /* Firefox */
}
.mobile-grain-picker-scroll::-webkit-scrollbar { display: none; }

.mobile-grain-pick {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  /* min-width, not width: wide glyphs (MFcr is two circles side by side)
   * widen their chip instead of spilling over its border. */
  min-width: 64px;
  min-height: 72px;
  padding: 6px 8px;
  background: var(--color-surface-alt);
  color: var(--color-text, inherit);
  border: 1px solid var(--color-border);
  border-radius: 0;
  cursor: pointer;
  scroll-snap-align: start;
  /* Disable the iOS double-tap-to-zoom delay on these buttons. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}
.mobile-grain-pick:active {
  background: var(--color-border);
  transform: scale(0.96);
}
.mobile-grain-pick.is-selected {
  border-color: var(--color-primary);
  background: color-mix(in srgb, var(--color-primary) 12%, var(--color-surface-alt));
}
.mobile-grain-pick .snow-symbol {
  font-size: 32px;
  line-height: 1;
}
/* The font's MFcr glyph ('O') declares an advance of 1615/2048 em but its
 * ink reaches 3707/2048 em, so layout sees it ~1em narrower than drawn.
 * Pad the difference so the chip sizes to the real glyph. */
.mobile-grain-pick[data-code="MFcr"] .snow-symbol {
  padding-right: 1.02em;
}
.mobile-grain-pick-code {
  font-size: var(--text-2xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-text-muted, var(--color-primary));
}

/* ── Embed mode (?embed=1) ───────────────────────────────────────────
 * Logo and action buttons stay. Language is passed in via ?lang so the
 * picker is dropped. */
[data-embed="1"] #sel-lang { display: none; }

/* The page scrollbar is normally suppressed (see the `html` rule near the
 * top of this file) and relies on the sticky tab bar as the "more below"
 * cue. Inside the host's modal that cue reads as part of a page the user
 * already scrolled past, so bring the scrollbar back as an explicit sign
 * that there's more content underneath. */
html[data-embed="1"] {
  scrollbar-width: thin;
}
html[data-embed="1"]::-webkit-scrollbar {
  width: 12px;
  height: 12px;
  display: block;
}
/* The preview dialog covers the page and scrolls its own body — while it's
 * open there's nothing left to scroll in the (now hidden) page underneath,
 * so drop back to no scrollbar rather than showing one for content that
 * isn't on screen. */
html[data-embed="1"]:has(#preview-dialog[open]) {
  scrollbar-width: none;
}
html[data-embed="1"]:has(#preview-dialog[open])::-webkit-scrollbar {
  display: none;
}

[data-embed="1"] .app-header { position: static; }
/* …so there is no sticky header to sit below: the tab bar sticks to the
 * top of the host's frame instead of leaving a header-sized gap of
 * scrolled-past content above it. */
[data-embed="1"] .tabs-bar { top: 0; }
[data-embed="1"] .app-main { padding-top: 0; }
/* Taller header gives the host modal's close button (top-right, ~42px inset
 * 10px from the dialog corner) vertical room so it doesn't touch the first
 * form row; padding-right keeps the Upload action left of it. min-height (not
 * height) so the mobile two-row layout can still grow. */
[data-embed="1"] .header-inner {
  height: auto;
  min-height: 72px;
  padding-right: 4.5rem;
}
/* Same accommodation for the preview dialog's own header — its rightmost
 * button (Back to edit) would otherwise sit under the host's close button. */
[data-embed="1"] .preview-dialog .dialog-header {
  padding-right: 4.5rem;
}
