/* tire-plant-map — Linear-inspired quiet B2B chrome
 *
 * Design system:
 *   - Primary: #5e6ad2 (Linear blue-violet) — chosen for industrial dashboard
 *     where the user stares at it for hours. Less "SaaS marketing" than Stripe
 *     indigo, less "dev tool" than GitHub blue.
 *   - Phase colors: green (operating) / amber (building) / blue (planned)
 *   - Quiet greys, white cards on near-white app bg
 *
 * Tokens:
 *   --bg-app      #fafbfc     near-white
 *   --bg-card     #ffffff     pure white
 *   --bg-subtle   #f6f7f9     table row, code blocks, input bg
 *   --border      #e5e7eb     default border
 *   --border-strong #d0d7de   input border
 *   --text-primary #1f2328    main text
 *   --text-secondary #57606a  muted but readable
 *   --text-muted  #8b949e     less important
 *   --text-disabled #afb8c1   placeholders
 *
 * Font sizes: 12 / 13 / 14 / 16 / 18 / 22 (6 stages)
 * Spacing: 4 / 8 / 12 / 16 / 24 / 32 (6 stages)
 * Radius: 4 / 6 / 8 (3 stages)
 */

:root {
  /* Type */
  /* Type — industrial / utilitarian direction.
     "IBM Plex Sans" is the design-system font for industrial data
     tools; the rest is a system fallback chain (no web font
     download, zero extra cost). The previous chain listed Inter +
     Roboto which the ui-design-guide skill flags as "AI-aesthetic
     defaults" — moving to Plex + Söhne positions us toward an
     editorial/industrial feel. */
  --font: "IBM Plex Sans", "Söhne", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  --font-mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;

  --text-2xs: 11px;
  --text-xs: 12px;
  --text-sm: 13px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-xl: 18px;
  --text-2xl: 22px;

  --weight-normal: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  /* Spacing */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;

  /* Radius */
  --radius-sm: 4px;
  --radius-md: 6px;
  --radius-lg: 8px;

  /* Color */
  --bg-app: #fafbfc;
  --bg-card: #ffffff;
  --bg-subtle: #f6f7f9;
  --bg-input: #ffffff;
  --bg-hover: #f3f4f6;
  --border: #e5e7eb;
  --border-strong: #d0d7de;

  --text-primary: #1f2328;
  --text-secondary: #57606a;
  --text-muted: #8b949e;
  --text-disabled: #afb8c1;

  /* Primary — cyan-700 (deep teal) for the industrial direction.
     The previous Linear blue-violet (#5e6ad2) read as a generic
     "AI SaaS" colour; cyan-700 sits in the same family as our
     phase-building token so the brand mark, focus ring, and
     "building" legend dot all share one hue. */
  --color-primary: #4f46e5;
  --color-primary-hover: #4338ca;
  --color-primary-bg: #eef2ff;
  --color-primary-border: #c7d2fe;

  --color-success: #1a7f37;
  --color-success-bg: #dafbe1;
  --color-success-border: #aceebb;

  --color-warning: #9a6700;
  --color-warning-bg: #fff8c5;
  --color-warning-border: #f0d661;

  --color-danger: #cf222e;
  --color-danger-hover: #a40e26;
  --color-danger-bg: #ffebe9;
  --color-danger-border: #ffcecb;

  /* Phase colors — Tailwind 500 triad (high-saturation, no color-hue overlap).
     green-500 / blue-500 / amber-500 — picked because the three hues sit
     120° apart on the color wheel (green/blue/orange) so the legend dots
     stay distinguishable even on the light v4 raster basemap. Brighter
     than the previous emerald-600/cyan-600/yellow-500 set: the old cyan
     and green shared a cool-tone family and read as "two greens" when
     several markers clustered on the same continent. The 100-tier tints
     are the matching badge backgrounds. */
  --color-phase-op: #22C55E;
  --color-phase-op-bg: #DCFCE7;
  --color-phase-bld: #3B82F6;
  --color-phase-bld-bg: #DBEAFE;
  --color-phase-plan: #F59E0B;
  --color-phase-plan-bg: #FEF3C7;

  /* Layout */
  --header-height: 56px;
  --filter-height: 36px;
  --control-height: 24px;
  --sidebar-width: 360px;
  --modal-max-width: 720px;
  --modal-narrow-max-width: 360px;

  /* Shadow */
  --shadow-sm: 0 1px 2px rgba(31, 35, 40, 0.04);
  --shadow-md: 0 4px 12px rgba(31, 35, 40, 0.08);
  --shadow-lg: 0 8px 24px rgba(31, 35, 40, 0.12);
  color-scheme: light;  /* 2026-07-19: 跟 auto/iir/feed/raw 统一 */
}

/* ──────── Reset / base ──────── */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: 1.5;
  color: var(--text-primary);
  background: var(--bg-app);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
code, pre, kbd, samp, .mono { font-family: var(--font-mono); font-size: var(--text-sm); }
h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: var(--weight-semibold); }
button { font-family: inherit; cursor: pointer; }
a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
input, select, textarea { font-family: inherit; font-size: var(--text-base); color: var(--text-primary); }

/* ──────── 2026-07-15 Vercel Web Interface Guidelines (sister file: china-iir-trade-dashboard/index.html) ──────── */
/* R1 — global :focus-visible ring (NOT :focus, 避免 mouse-click focus ring) */
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role="tab"]:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: 4px;
}
/* R11 — explicit button type reinforces semantic clarity */

/* R3 — prefers-reduced-motion: 杀 animations + transitions 全局,
   loading spinner 仍渲染 (border-top-color → border). */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ──────── App shell ──────── */
#app { display: flex; flex-direction: column; height: 100vh; }

/* ──────── ICP beian (法规要求, 浮在 viewport 左下角) ──────── */
#beian-link {
  position: fixed;
  bottom: 0;
  left: 0;
  z-index: 1000;
  padding: 4px 8px;
  /* 2026-07-15: iPhone home indicator 余量 (Vercel WIG safe-area). 不挡
     system UI 让徽章可点. */
  padding-bottom: calc(4px + env(safe-area-inset-bottom, 0));
  padding-left: calc(8px + env(safe-area-inset-left, 0));
  padding-right: calc(8px + env(safe-area-inset-right, 0));
  font-size: var(--text-2xs);
  color: var(--text-muted);
  background: #ffffff;
  border-top-right-radius: 4px;
  pointer-events: auto;
}
#beian-link a { color: var(--text-muted); text-decoration: none; }
#beian-link a:hover { color: var(--text-secondary); text-decoration: underline; }
#beian-link .beian-sep { margin: 0 6px; color: var(--text-muted); }
#beian-link .beian-police {
  display: inline-block;
  width: 14px; height: 14px;
  vertical-align: -2px;
  margin-right: 3px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'><path fill='%23e10602' d='M512 64 128 192v288c0 254 160 446 384 480 224-34 384-226 384-480V192L512 64z'/><path fill='%23fff' d='M512 220 280 296v178c0 154 96 270 232 290 136-20 232-136 232-290V296L512 220z'/><path fill='%23e10602' d='M512 350 380 388v78c0 76 50 134 132 144 82-10 132-68 132-144v-78L512 350z'/><path fill='%23ffd700' d='m442 482 50 50 100-100 30 30-130 130-80-80z'/></svg>");
  background-size: contain;
  background-repeat: no-repeat;
}

/* ──────── Header (3-row layout) ──────── */
#app > header {
  display: flex;
  flex-direction: column;
  background: var(--bg-card);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  z-index: 10;
  /* 2026-07-15: iPhone notch / safe area (Vercel WIG). 顶部 padding 让 home indicator 不挡内容,
     配合 <meta viewport="viewport-fit=cover"> 已加. */
  padding-top: env(safe-area-inset-top, 0);
  padding-left: env(safe-area-inset-left, 0);
  padding-right: env(safe-area-inset-right, 0);
}

.header-top {
  display: flex;
  align-items: center;
  height: var(--header-height);
  padding: 0 var(--space-4);
  gap: var(--space-4);
}
.header-brand {
  /* R10: 现在是 <h1> 元素, 继承 h1 reset 的 margin:0 + semibold. Brand 完整
     写法已经对齐 IIR/auto dashboard (display:flex + brand dot + text). */
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
}
.header-brand .brand-dot {
  width: 20px; height: 20px;
  border-radius: 5px;
  /* Solid primary swatch — the previous purple→violet gradient
     read as "AI aetheric glow" which the ui-design-guide skill
     explicitly forbids. A flat cyan-700 square is calmer and
     consistent with the rest of the cyan-family accents. */
  background: var(--color-primary);
  flex-shrink: 0;
}
.header-count {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 10px;
  border-radius: 999px;
  background: var(--bg-subtle);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  border: 1px solid var(--border);
}
.header-spacer {
  /* Desktop: pushes user-bar to the right. On mobile (max-width:
   * 720px) we hide it because the wrap layout would let it claim
   * an entire phantom row; we use margin-left:auto on user-bar and
   * add-btn instead so they still pin to the right. */
  flex: 1;
}

.header-filters {
  display: flex;
  align-items: center;
  height: var(--filter-height);
  padding: 0 var(--space-4);
  gap: var(--space-3);
  border-top: 1px solid var(--border);
  background: var(--bg-card);
  overflow-x: auto;
}

/* ──────── Legend (phase toggle)
   All header controls in .header-filters use the same chrome: white
   background, neutral border, 32px height, primary-color active state.
   No "segmented" gray-container style — that fights with the other
   controls' white-and-border style.
   The 3 items (Operating/Building/Planned) share the width of the legend
   container equally via flex: 1, so they line up regardless of label length. ──────── */
.legend {
  display: flex;
  align-items: stretch;
  height: var(--control-height);
  gap: 6px;
  background: transparent;
  border-radius: var(--radius-md);
  min-width: 280px;
}
/* ──────── Phase filter (3 dots) — pill-group 拨杆 (IIR/.seg parity).
   2026-07-15: 改为灰胶囊容器 + 透明浮起按钮, 跟 IIR dashboard 的 .seg/.tabs
   视觉一致 (tiremap 之前是"独立白卡 + border" GitHub 风, 跟 IIR 不同). ──────── */
.legend {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  min-height: 32px;
  padding: 2px;
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 999px;
  min-width: 0;
}
.legend-item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 28px;
  padding: 0 14px;
  background: transparent;
  border: none;
  border-radius: 999px;
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 1;
  /* R2 — specific props (Vercel WIG). 加 box-shadow 因为 active 浮起有阴影 */
  transition: background-color 0.12s, color 0.12s, box-shadow 0.12s;
  white-space: nowrap;
}
.legend-item .legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.legend-item:hover:not(.active) { color: var(--text-primary); }
.legend-item.active {
  background: var(--bg-card);
  color: var(--color-primary);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}

/* "View:" label — sits BETWEEN the two pills (phase pill + view pill),
   outside both containers, not part of any pill bg. Height matched
   to pill height (28px) so vertical centering in .header-filters
   keeps "View:" on the same baseline as the pill text inside. */
.legend-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  padding: 0 var(--space-2);
  height: 28px;
  line-height: 1;
  white-space: nowrap;
  flex-shrink: 0;
}

/* ──────── Sub-row wrappers (desktop = transparent, mobile = flex row)
   These wrap "View:" label + view pill, and brand-filter + add-plant,
   so mobile can keep them on the same row instead of stacking. On
   desktop `display: contents` makes the wrapper invisible (no extra
   box, no flex impact on children). ──────── */
.view-controls,
.brand-controls { display: contents; }

/* View-toggle buttons live inside the second .legend pill (which uses
   .legend styles too via the class combo "legend view-toggle"). No
   extra styles needed — inherits .legend-item chrome. */

/* ──────── Brand dropdown — same 32px outer height ──────── */
.brand-filter { position: relative; display: flex; align-items: center; }
.brand-btn {
  display: inline-flex; align-items: center; justify-content: space-between;
  gap: 8px;
  height: var(--control-height);
  padding: 0 10px;
  background: var(--bg-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 1;
  min-width: 130px;
}
.brand-btn:hover { border-color: var(--text-muted); }
.brand-btn-caret { color: var(--text-muted); font-size: var(--text-2xs); flex-shrink: 0; }
.brand-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  min-width: 240px;
  max-height: 360px;
  overflow-y: auto;
  background: var(--bg-card);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 1000;
  display: none;
}
.brand-panel.open { display: block; }
.brand-panel-actions { display: flex; gap: 4px; padding: var(--space-2); border-bottom: 1px solid var(--border); }
.brand-panel-actions button {
  flex: 1;
  padding: 4px 8px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: var(--text-xs);
}
.brand-panel-actions button:hover { background: var(--bg-subtle); color: var(--text-primary); }
.brand-list { padding: var(--space-1); }
.brand-row {
  /* Grid layout — the logo column is a fixed-width slot so brand names
   * line up vertically even when logos vary in width (Zhongce is much
   * narrower than Sailun). Without fixed columns, names drift with the
   * logo's right edge. */
  display: grid;
  grid-template-columns: auto 100px 1fr auto;
  align-items: center;
  column-gap: var(--space-2);
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: var(--text-xs);
}
.brand-row:hover { background: var(--bg-subtle); }
.brand-row input[type=checkbox] { margin: 0; cursor: pointer; }
.brand-row .brand-logo { justify-self: start; }
/* Empty placeholder when a brand has no logo — keeps the brand-name
 * column aligned with the rows that do have a logo. */
.brand-logo-slot { display: inline-block; width: 96px; height: 28px; }
.brand-name { color: var(--text-primary); }
.brand-count { color: var(--text-muted); font-size: var(--text-2xs); }

/* Brand logos — scaled to fit a fixed envelope so the row height stays
 * predictable regardless of the source PNG aspect ratio (some wordmarks
 * are very wide-and-short, others nearly square). object-fit: contain
 * means we never crop or distort; the logo sits in a clean transparent
 * box that aligns with the text baseline. */
.brand-logo {
  max-width: 96px;
  max-height: 28px;
  width: auto;
  height: auto;
  object-fit: contain;
  flex-shrink: 0;
  display: block;
}
/* Inside the trigger button — slightly smaller, sit flush with the label text. */
.brand-logo-btn {
  max-width: 80px;
  max-height: 22px;
}

/* ──────── Buttons ──────── */
.btn-primary, .btn-secondary, .btn-danger, .btn-success {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 32px;
  padding: 0 14px;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  border: 1px solid transparent;
  /* R2 — specific props (Vercel WIG) */
  transition: background-color 0.12s, border-color 0.12s, color 0.12s, opacity 0.12s;
  cursor: pointer;
  white-space: nowrap;
}
.btn-primary { background: var(--color-primary); color: white; border-color: var(--color-primary); }
.btn-primary:hover { background: var(--color-primary-hover); border-color: var(--color-primary-hover); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-secondary { background: var(--bg-card); color: var(--text-primary); border-color: var(--border-strong); }
.btn-secondary:hover { background: var(--bg-subtle); border-color: var(--text-muted); }
.btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-danger { background: var(--bg-card); color: var(--color-danger); border-color: var(--color-danger-border); }
.btn-danger:hover { background: var(--color-danger-bg); border-color: var(--color-danger); }

.btn-success { background: var(--bg-card); color: var(--color-success); border-color: var(--color-success-border); }
.btn-success:hover { background: var(--color-success-bg); border-color: var(--color-success); }

.btn-ghost { background: transparent; color: var(--text-secondary); border: none; padding: 4px 8px; font-size: var(--text-sm); }
.btn-ghost:hover { color: var(--text-primary); background: var(--bg-subtle); border-radius: var(--radius-sm); }

/* Add plant button — same 32px height as other header controls. Uses
   primary color for text + border (outline style) so the CTA still reads
   as primary but shares the white background with the filter controls. */
.add-btn {
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--control-height);
  padding: 0 12px;
  background: var(--bg-card);
  color: var(--color-primary);
  border: 1px solid var(--color-primary);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  line-height: 1;
  /* R2 — specific props (Vercel WIG) */
  transition: background-color 0.12s, border-color 0.12s, color 0.12s;
}
.add-btn:hover {
  background: var(--color-primary);
  color: white;
}

/* ──────── User bar widget (top-right) ──────── */
.user-bar { display: inline-flex; align-items: center; gap: var(--space-2); }
.user-bar-name { font-size: var(--text-sm); color: var(--text-primary); font-weight: var(--weight-medium); }
.user-bar-role { font-size: var(--text-2xs); padding: 1px 6px; border-radius: var(--radius-sm); background: var(--bg-subtle); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.4px; font-weight: var(--weight-semibold); }
.user-bar button { font-size: var(--text-sm); }
/* The "Sign in" button rendered by auth.js when nobody is logged in.
   Inherits the .btn-secondary chrome (white card + neutral border)
   so it sits naturally next to the "31 plants" count chip and the
   "+ Add plant" CTA, but sized for the 56px header row. */
.user-bar-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px;
  height: var(--control-height);
  padding: 0 12px;
  background: var(--bg-card);
  color: var(--text-primary);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 1;
  cursor: pointer;
  /* R2 — specific props (Vercel WIG) */
  transition: background-color 0.12s, border-color 0.12s, color 0.12s;
}
.user-bar-btn:hover { background: var(--bg-subtle); border-color: var(--text-muted); }

/* ──────── Map ──────── */
#map { flex: 1; }
.maplibregl-ctrl-group { border-radius: var(--radius-md) !important; box-shadow: var(--shadow-md) !important; }
.maplibregl-popup-content {
  padding: var(--space-3) var(--space-4) !important;
  border-radius: var(--radius-md) !important;
  box-shadow: var(--shadow-lg) !important;
  font-family: var(--font) !important;
  font-size: var(--text-sm) !important;
}
.maplibregl-popup-content strong { font-size: var(--text-sm); color: var(--text-primary); }
.maplibregl-popup-content .meta { font-size: var(--text-xs); color: var(--text-muted); margin-top: 2px; }
.maplibregl-popup-content .cap { font-size: var(--text-2xs); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.4px; font-weight: var(--weight-semibold); }
.popup-cap-row { display: flex; justify-content: space-between; padding: 3px 0; font-size: var(--text-sm); }
.popup-cap-row .cap-label { color: var(--text-secondary); }
.popup-cap-row .cap-val { font-weight: var(--weight-medium); font-variant-numeric: tabular-nums; }
.popup-cap-row .cap-val.zero { color: var(--text-disabled); }

/* ──────── Modal ──────── */
.modal {
  position: fixed; inset: 0;
  z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
  overflow-y: auto;
}
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(31, 35, 40, 0.4); backdrop-filter: blur(2px); }
.modal-panel {
  position: relative;
  width: 100%;
  max-width: var(--modal-max-width);
  background: var(--bg-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column;
  max-height: calc(100vh - 80px);
}
.modal-panel-narrow { max-width: var(--modal-narrow-max-width); }
.modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.modal-header h2 { font-size: var(--text-lg); font-weight: var(--weight-semibold); }
.modal-x, .modal-close {
  background: transparent; border: none;
  color: var(--text-muted);
  font-size: 20px; line-height: 1;
  width: 28px; height: 28px;
  border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.modal-x:hover, .modal-close:hover { background: var(--bg-subtle); color: var(--text-primary); }

.modal-body {
  padding: var(--space-5);
  overflow-y: auto;
  flex: 1;
}
.modal-footer {
  display: flex; justify-content: flex-end; gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border);
  background: var(--bg-subtle);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  flex-shrink: 0;
}
#login-modal .modal-footer { justify-content: flex-end; }
#login-modal .modal-footer button { min-width: 88px; }

/* ──────── Form sections (no boxes, just headers + lines) ──────── */
.form-section { border: none; padding: 0; margin: 0 0 var(--space-5); }
.form-section:last-child { margin-bottom: 0; }
.form-section > legend {
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-secondary);
  padding: 0 0 var(--space-2);
  margin: 0;
  border-bottom: 1px solid var(--border);
  width: 100%;
  display: block;
  margin-bottom: var(--space-3);
}
.form-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--space-3); margin-bottom: var(--space-3); }
.form-row:last-child { margin-bottom: 0; }
.form-row label, .form-section > label {
  display: block;
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  margin-bottom: 4px;
}
.form-row label.full { grid-column: 1 / -1; }
.form-row input, .form-row select, .form-section > label input, .form-section > label select {
  display: block;
  width: 100%;
  padding: 5px 10px;
  background: var(--bg-input);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--text-primary);
  transition: border-color 0.12s, box-shadow 0.12s;
  margin-top: 4px;
}
.form-row input:focus, .form-row select:focus, .form-section > label input:focus, .form-section > label select:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-bg);
}
.form-row input::placeholder, .form-section > label input::placeholder { color: var(--text-disabled); }
.form-row label > span.hint, .form-section > label > span.hint {
  font-weight: var(--weight-normal);
  color: var(--text-muted);
  font-size: var(--text-2xs);
  margin-left: 4px;
}

.form-error {
  margin: var(--space-3) 0 0;
  padding: 8px 12px;
  background: var(--color-danger-bg);
  color: var(--color-danger);
  border: 1px solid var(--color-danger-border);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}

/* Login form is just stacked labels. Add horizontal padding so inputs
   don't run flush against the modal-panel edges (the panel itself has
   no padding, only .modal-header/.modal-footer do). */
#login-form { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
#login-form label {
  display: flex; flex-direction: column;
  font-size: var(--text-2xs);
  color: var(--text-secondary);
  font-weight: var(--weight-medium);
  gap: 4px;
}
#login-form input {
  padding: 5px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  background: var(--bg-input);
  color: var(--text-primary);
  height: 32px;
  box-sizing: border-box;
}
#login-form input:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-bg); }
#login-display-name-row[hidden] { display: none; }
.form-error {
  background: var(--color-danger-bg);
  color: var(--color-danger);
  border: 1px solid var(--color-danger-border);
  border-radius: var(--radius-sm);
  padding: 6px 10px;
  font-size: var(--text-sm);
  margin-top: 4px;
}
.login-switch { margin-top: var(--space-2); text-align: center; font-size: var(--text-xs); }
.login-switch a { color: var(--color-primary); }
.login-switch a:hover { text-decoration: underline; }

/* ──────── Sidebar (right drawer) ──────── */
#sidebar {
  position: fixed;
  top: 0; right: 0; bottom: 0;
  width: var(--sidebar-width);
  max-width: 90vw;
  background: var(--bg-card);
  border-left: 1px solid var(--border);
  box-shadow: var(--shadow-lg);
  z-index: 50;
  display: flex;
  flex-direction: column;
  animation: slide-in 0.18s ease-out;
}
@keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
#sidebar[hidden], #sidebar.hidden { display: none !important; }
#sidebar .close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 22px;
  line-height: 1;
  width: 28px; height: 28px;
  border-radius: var(--radius-sm);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer;
  z-index: 2;
}
#sidebar .close:hover { background: var(--bg-subtle); color: var(--text-primary); }
#sidebar-title {
  padding: var(--space-4) var(--space-5) var(--space-3);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  border-bottom: 1px solid var(--border);
  padding-right: 40px;
}
#sidebar-body {
  flex: 1;
  overflow-y: auto;
  padding: 0 0 var(--space-5);
}

/* Sidebar section headers */
.sb-section { padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border); }
.sb-section:last-of-type { border-bottom: none; }
.sb-section-title {
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-muted);
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-3);
}

/* Sidebar detail rows */
.detail-row { display: flex; justify-content: space-between; gap: var(--space-3); padding: 4px 0; font-size: var(--text-sm); }
.detail-row .label { color: var(--text-secondary); flex-shrink: 0; }
.detail-row .value { color: var(--text-primary); font-weight: var(--weight-medium); text-align: right; }
.detail-row .value.muted { color: var(--text-muted); font-style: italic; font-weight: var(--weight-normal); }
.detail-row .value .mono { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-muted); }

/* Sidebar capacity rows with mini bar */
.cap-section { margin-top: var(--space-2); }
.cap-row-split { display: grid; grid-template-columns: 36px 1fr; gap: var(--space-2); padding: 4px 0; align-items: center; }
.cap-row-split .type { font-size: var(--text-xs); color: var(--text-secondary); font-weight: var(--weight-medium); }
.cap-row-split .val { display: flex; align-items: center; gap: 6px; justify-content: flex-end; }
.cap-row-split .num { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-primary); font-variant-numeric: tabular-nums; }
.cap-row-split .unit { font-size: var(--text-2xs); color: var(--text-muted); }
.cap-row-split.muted .num, .cap-row-split.muted .val { color: var(--text-disabled); }
.cap-bar { display: inline-block; flex: 1; height: 4px; background: var(--bg-subtle); border-radius: 2px; overflow: hidden; max-width: 100px; }
.cap-bar-fill { height: 100%; background: var(--color-primary); border-radius: 2px; }

/* Phase badge */
.phase-badge {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.4px;
}
.phase-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; }
.phase-badge.operating { background: var(--color-phase-op-bg); color: var(--color-phase-op); }
.phase-badge.operating::before { background: var(--color-phase-op); }
.phase-badge.building { background: var(--color-phase-bld-bg); color: var(--color-phase-bld); }
.phase-badge.building::before { background: var(--color-phase-bld); }
.phase-badge.planned { background: var(--color-phase-plan-bg); color: var(--color-phase-plan); }
.phase-badge.planned::before { background: var(--color-phase-plan); }

/* Sidebar action footer — pinned to bottom via flex layout.
   #sidebar-body is the scrollable middle; the action footer always sits
   below it so the buttons are visible regardless of sidebar content length. */
.sidebar-actions {
  display: flex; gap: var(--space-2);
  padding: var(--space-3) var(--space-5);
  border-top: 1px solid var(--border);
  background: var(--bg-card);
  flex-shrink: 0;
}
.sidebar-actions button { flex: 1; }

/* ──────── Admin page ──────── */
.admin-page {
  max-width: 1100px;
  margin: 0 auto;
  padding: var(--space-5) var(--space-4);
}
.admin-page h1 {
  font-size: var(--text-2xl);
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-5);
}
.admin-page h2 {
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-secondary);
  margin: 0 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.panel {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  margin-bottom: var(--space-4);
  box-shadow: var(--shadow-sm);
}
.panel-row { color: var(--text-muted); font-size: var(--text-sm); }
.panel-row b { color: var(--text-primary); }

/* ──────── Sign-in gate (admin.html, unauth state) ────────
   A centred, narrow card with one obvious CTA. Industrial-style
   minimal — no marketing copy, no hero illustration, just a lock
   icon + 1-line title + 1-line sub + 1 button. Matches the
   "industrial/utilitarian" aesthetic the rest of the site uses. */
.signin-gate {
  min-height: calc(100vh - 200px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-6) var(--space-4);
}
.signin-gate-card {
  width: 100%;
  max-width: 380px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-6) var(--space-5);
  text-align: center;
}
.signin-gate-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  margin: 0 auto var(--space-4);
  border-radius: var(--radius-md);
  background: var(--color-primary-bg);
  color: var(--color-primary);
}
.signin-gate-icon .icon { width: 24px; height: 24px; }
.signin-gate-title {
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  margin: 0 0 var(--space-2);
}
.signin-gate-sub {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0 0 var(--space-5);
  line-height: 1.5;
}
.signin-gate-card .btn-primary {
  width: 100%;
  justify-content: center;
  height: var(--control-height);
}

/* New user form (5-column) */
.new-user-form {
  display: grid;
  grid-template-columns: 1.2fr 1.5fr 1.2fr 0.8fr auto;
  gap: var(--space-3);
  align-items: end;
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
.new-user-form > div { display: flex; flex-direction: column; }
.new-user-form label { font-size: var(--text-2xs); color: var(--text-secondary); margin-bottom: 4px; font-weight: var(--weight-medium); }
.new-user-form input, .new-user-form select {
  padding: 5px 10px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  background: var(--bg-input);
}
.new-user-form input:focus, .new-user-form select:focus { outline: none; border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-bg); }
.new-user-form button { height: 30px; }

/* Admin tables */
.admin-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.admin-table thead th {
  background: var(--bg-subtle);
  font-weight: var(--weight-semibold);
  color: var(--text-secondary);
  font-size: var(--text-2xs);
  text-transform: uppercase;
  letter-spacing: 0.4px;
  text-align: left;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.admin-table tbody td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.admin-table tbody tr:hover { background: var(--bg-subtle); }
.admin-table tbody tr:last-child td { border-bottom: none; }
.admin-table .actions { white-space: nowrap; }
.admin-table .actions button { font-size: var(--text-xs); padding: 3px 10px; margin-right: 4px; }
.admin-table .actions button:last-child { margin-right: 0; }
.admin-table .num-col { text-align: center; font-variant-numeric: tabular-nums; }
.admin-table .muted-cell { color: var(--text-muted); font-size: var(--text-xs); }
.admin-table .mono-cell { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-muted); white-space: nowrap; }

/* Inline link-style action button (e.g. "Restore" inside an audit-log
   cell). Sized to match the surrounding cell text (--text-sm) so the
   button doesn't look oversized next to inline content. Uses the
   success color to mirror the older .btn-success style, but is much
   smaller: a hairline border, tight padding, no extra chrome. */
.btn-link-action {
  display: inline-flex;
  align-items: center;
  padding: 1px 7px;
  margin-left: 6px;
  border: 1px solid var(--color-success-border);
  border-radius: var(--radius-sm);
  background: var(--bg-card);
  color: var(--color-success);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  font-family: inherit;
  cursor: pointer;
  line-height: 1.4;
  /* R2 — specific props (Vercel WIG) */
  transition: background-color 0.12s, border-color 0.12s, color 0.12s;
  vertical-align: baseline;
  white-space: nowrap;
}
.btn-link-action:hover {
  background: var(--color-success-bg);
  border-color: var(--color-success);
}

/* Role pills (toggle group) — pill-group 拨杆 (IIR parity).
   2026-07-15: 之前 active 用 filled color (admin 橙 / editor 蓝 / viewer 灰), 跟 IIR /
   legend / view-toggle 的浮起白卡 active 不一致. 改为统一浮起 chrome + 每个 role
   仍然用 brand 色区分 (透明 active + color). */
.role-pills {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  background: var(--bg-subtle);
  border: 1px solid var(--border);
  border-radius: 999px;
}
.role-pill {
  flex: 1; min-width: 64px;
  height: 26px;
  padding: 0 12px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent;
  border-radius: 999px;
  font-size: var(--text-2xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.4px;
  cursor: pointer;
  color: var(--text-secondary);
  /* R2 — specific props (Vercel WIG). 加 box-shadow 因为 active 浮起 */
  transition: background-color 0.12s, color 0.12s, box-shadow 0.12s;
}
.role-pill:hover:not(:disabled):not(.active) { color: var(--text-primary); }
.role-pill:disabled { cursor: not-allowed; opacity: 0.5; }
.role-pill.active { cursor: default; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); }
.role-pill.active { background: var(--bg-card); color: var(--text-primary); }
.role-pill.active.role-admin { color: var(--color-warning); }
.role-pill.active.role-editor { color: var(--color-primary); }
.role-pill.active.role-viewer { color: var(--text-secondary); }

/* ──────── Admin page mobile responsive (≤ 720px) ────────
   The admin shell has zero native responsive — tables overflow
   viewport, new-user-form is 5-column grid, signin-gate is fixed
   380px. Apply the same tight spacing as the dashboard mobile CSS.
   ─────────────────────────────────────────────────────────── */
@media (max-width: 720px) {
  .admin-page {
    padding: var(--space-3) var(--space-3);
  }
  .admin-page h1 {
    font-size: var(--text-xl);
    margin: 0 0 var(--space-3);
    padding: 0 var(--space-2);
  }
  .admin-page h2 {
    font-size: var(--text-2xs);
    margin: 0 0 var(--space-2);
    padding: 0 var(--space-2);
  }
  .back-link { padding: var(--space-2) var(--space-3); }

  .panel {
    padding: var(--space-3);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-3);
  }

  /* Sign-in gate — drop max-width, use viewport width minus padding */
  .signin-gate {
    padding: var(--space-4) var(--space-3);
    min-height: calc(100vh - 160px);
  }
  .signin-gate-card {
    max-width: none;
    padding: var(--space-5) var(--space-4);
  }

  /* New-user 5-column grid → 1-column stack */
  .new-user-form {
    grid-template-columns: 1fr;
    gap: var(--space-2);
  }
  .new-user-form button {
    width: 100%;
    margin-top: var(--space-1);
  }

  /* Admin tables → wrap in horizontal scroll container so wide
     tables (audit: 5 cols, users: 7 cols) don't push the page
     wider than the viewport. The container exposes a thin shadow
     at the right edge to hint at overflow, matching GitHub/GitLab. */
  .table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    margin: 0 calc(-1 * var(--space-3));
    padding: 0 var(--space-3);
  }
  .table-scroll::after {
    content: "";
    display: block;
    position: sticky;
    right: 0;
    /* subtle hint at right edge for visual overflow */
  }
  .admin-table {
    min-width: 540px;          /* forces horizontal scroll on phones */
    font-size: var(--text-xs);
  }
  .admin-table thead th {
    padding: var(--space-2) var(--space-2);
    font-size: var(--text-2xs);
  }
  .admin-table tbody td {
    padding: var(--space-2) var(--space-2);
  }
  .admin-table .actions button {
    padding: 4px 8px;
    margin-right: 4px;
  }
  /* Smaller phones: tables reflow into stacked cards. Each <tr>
     becomes a card and each <td> gets its column header from
     data-label as a ::before pseudo-element. The horizontal
     scroll container keeps overflow:visible so the cards lay out
     vertically. */
  .table-scroll { overflow-x: visible; }
  .admin-table,
  .admin-table thead,
  .admin-table tbody,
  .admin-table tr,
  .admin-table td,
  .admin-table th {
    display: block;
  }
  .admin-table thead { display: none; }
  .admin-table {
    min-width: 0;
    width: 100%;
    border: 0;
  }
  .admin-table tbody tr {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: var(--space-2) var(--space-3);
    margin-bottom: var(--space-2);
    background: var(--bg-card);
  }
  .admin-table tbody td {
    display: flex;
    justify-content: space-between;
    align-items: center;
    border: none;
    border-bottom: 1px solid var(--border);
    padding: 6px 0;
    text-align: right;
  }
  .admin-table tbody td:last-child { border-bottom: none; padding-bottom: 0; }
  .admin-table tbody td::before {
    content: attr(data-label);
    font-size: var(--text-2xs);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    font-weight: var(--weight-semibold);
    text-align: left;
    margin-right: var(--space-3);
    flex-shrink: 0;
    max-width: 45%;
  }
  .admin-table tbody td.actions {
    display: block;
    text-align: left;
    padding-top: var(--space-2);
  }
  .admin-table tbody td.actions::before { display: none; }

  /* Role pills — shorter label, less padding on phones. The full
     text remains in the title attribute for screen readers / hover. */
  .role-pill {
    min-width: 56px;
    padding: 0 8px;
    font-size: 10px;
  }
}

@media (max-width: 380px) {
  .admin-page { padding: var(--space-2) var(--space-2); }
  .admin-page h1 { font-size: var(--text-lg); }
  .signin-gate-card { padding: var(--space-4) var(--space-3); }
  .signin-gate-icon { width: 40px; height: 40px; }
  .signin-gate-icon .icon { width: 20px; height: 20px; }
  .role-pill { min-width: 48px; padding: 0 6px; font-size: 9px; }
}

/* Edits count cell highlight */
.edits-count { font-weight: var(--weight-semibold); }
.edits-count.zero { color: var(--text-disabled); }
.edits-count.has-edits { color: var(--color-primary); }

/* Audit log diff cell — JSON may be wide, so wrap + max-height + scroll.
   Selector targets .actions cells inside .admin-table (the audit-table is
   rendered into #audit-table as a .admin-table; the diff sits in a
   .actions <td>). white-space: pre-wrap so long lines wrap instead of
   pushing the column wider. */
.admin-table td.actions pre {
  display: block;
  font-size: var(--text-2xs);
  font-family: var(--font-mono);
  color: var(--text-secondary);
  max-width: 480px;
  max-height: 100px;
  overflow: auto;
  background: var(--bg-subtle);
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  margin: 0;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Toast */
.toast {
  position: fixed;
  bottom: var(--space-5);
  right: var(--space-5);
  padding: 10px 16px;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  z-index: 2000;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.18s, transform 0.18s;
  box-shadow: var(--shadow-md);
  font-weight: var(--weight-medium);
  max-width: 360px;
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.ok { background: var(--color-success-bg); color: var(--color-success); border: 1px solid var(--color-success-border); }
.toast.err { background: var(--color-danger-bg); color: var(--color-danger); border: 1px solid var(--color-danger-border); }

/* Pagination */
.pagination { display: flex; gap: var(--space-2); margin-top: var(--space-3); align-items: center; font-size: var(--text-xs); color: var(--text-muted); }
.pagination button { font-size: var(--text-xs); padding: 4px 10px; }
.pagination .page-info { margin-left: var(--space-2); }

/* Back to map link (admin.html) */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  margin-bottom: var(--space-3);
}
.back-link:hover { color: var(--color-primary); text-decoration: none; }

/* Utility */
.text-muted { color: var(--text-muted); }
.text-secondary { color: var(--text-secondary); }
.text-primary-color { color: var(--color-primary); }
.text-success { color: var(--color-success); }
.text-disabled { color: var(--text-disabled); font-style: italic; }
.text-xs { font-size: var(--text-xs); }
.text-center { text-align: center; }
.mono { font-family: var(--font-mono); }
.mb-1 { margin-bottom: var(--space-1); }
.mb-2 { margin-bottom: var(--space-2); }
.mb-3 { margin-bottom: var(--space-3); }
.mb-4 { margin-bottom: var(--space-4); }
.mt-1 { margin-top: var(--space-1); }
.mt-2 { margin-top: var(--space-2); }
.mt-3 { margin-top: var(--space-3); }
.mt-4 { margin-top: var(--space-4); }

/* Sidebar capacity unit-note italic */
.cap-unit-note {
  font-size: var(--text-2xs);
  color: var(--text-muted);
  margin-top: var(--space-1);
  font-style: italic;
  padding: var(--space-1) 0 0;
  border-top: 1px dashed var(--border);
}

/* ──────── Inline icons (Lucide, inlined) ────────
   Icons are inlined as <svg> in the HTML, with stroke="currentColor"
   so they inherit colour from the surrounding text. The .icon class
   just controls size and vertical alignment. No HTTP request, no JS,
   no filter hack — currentColor is honoured by every browser. */
.icon {
  display: inline-block;
  vertical-align: -0.125em;
  width: 1em;
  height: 1em;
  flex-shrink: 0;
  stroke: currentColor;
}
.icon-sm { width: 0.85em; height: 0.85em; }
.icon-lg { width: 1.25em; height: 1.25em; }
/* When an icon sits inside a button, give it a small right margin
   so the icon-text pair reads as one unit. */
button > .icon + span,
a > .icon + span { margin-left: 0.35em; }

/* ──────── Responsive (≤ 720px) ────────
   The desktop layout is built for 1024+ viewports: a wide sidebar drawer,
   a 4-column header-filters row, and a 4-column brand-row grid. On
   phones the wide drawer eats the whole map, the filters overflow
   horizontally, and long brand names like "Prinx Chengshan" wrap.
   This block reflows for narrow viewports without touching the desktop
   styles. */
@media (max-width: 720px) {
  /* Header — let the title wrap below the filter row when narrow */
  .header-top {
    flex-wrap: wrap;
    height: auto;
    padding: var(--space-2) var(--space-3);
    gap: var(--space-2);
  }
  .header-brand {
    font-size: var(--text-md);
  }
  .header-count { font-size: var(--text-2xs); }
  /* header-spacer exists to push the user-bar to the right on desktop.
   * On phones it would claim an entire row when flex-wrap kicks in,
   * so we hide it here and pin user-bar/add-btn to the right edge via
   * margin-left:auto instead. Sign-in then lines up with "+ Add plant"
   * below it (both share the right border). */
  .header-spacer { display: none; }
  .user-bar { margin-left: auto; }
  .add-btn { margin-left: auto; }

  /* Filter strip — switch to column flex so each row is a deterministic
   * line (no flex-wrap surprises), and pills fill the row width via
   * align-self: stretch rather than flex-basis: 100% (which would force
   * them past their content). */
  .header-filters {
    flex-direction: column;
    height: auto;
    padding: var(--space-2) var(--space-3);
    gap: var(--space-2);
    overflow-x: visible;
    align-items: stretch;
  }

  /* Phase pill — own row, fit-content pill bg stretched to row width.
   * .legend is inline-flex (fit content) on desktop; on phones we want
   * the bg to span the row, so we override display to flex + stretch. */
  .legend {
    display: flex;
    width: auto;
    align-self: stretch;
    min-width: 0;
  }
  /* The 3 phase items (Operating/Building/Planned) split the pill
   * evenly across the row on phones so the pill has clear tap targets. */
  .legend-item {
    flex: 1 1 0;
    min-width: 0;
  }
  /* Sub-row wrappers become real flex containers on mobile so the
   * "View:" label sits on the same row as the view pill, and the
   * brand filter shares a row with the "Add plant" button. */
  .view-controls,
  .brand-controls {
    display: flex;
    align-items: stretch;
    gap: var(--space-2);
    align-self: stretch;
  }
  .view-controls .legend-label { flex: 0 0 auto; }
  .view-controls .legend.view-toggle { flex: 1 1 auto; min-width: 0; }
  /* Brand row: brand-filter grows to fill leftover row space, spacer
   * hidden (already), add-plant stays auto-width pinned to right. */
  .brand-controls .brand-filter { flex: 1 1 auto; min-width: 0; }
  .brand-controls .header-spacer { display: none; }
  /* "All brands / 3 selected / Triangle + logo" can be long; let the
   * label wrap to a second line instead of pushing the row wider. */
  .brand-btn-label {
    white-space: normal;
    text-align: left;
    flex: 1;
    line-height: 1.2;
  }
  .brand-panel {
    /* Don't anchor to a tiny button — span the whole viewport. */
    left: var(--space-2);
    right: var(--space-2);
    min-width: 0;
    max-height: 70vh;
  }

  /* "Add plant" CTA — collapse to icon-only on phones so it doesn't
   * force the filter row wider than the viewport. */
  .add-btn { padding: 0 8px; flex: 0 0 auto; align-self: stretch; }
  .add-btn span { display: none; }

  /* Brand row — drop the logo slot to 64px and tighten the gaps so a
   * 320px viewport still fits "Prinx Chengshan" on one line. The 1fr
   * name column lets long names compress but never overflows because
   * the dropdown itself spans the viewport. */
  .brand-row {
    grid-template-columns: auto 64px 1fr auto;
    column-gap: 6px;
    padding: 6px 4px;
  }
  .brand-row .brand-logo,
  .brand-logo-slot {
    max-width: 60px;
    max-height: 24px;
    width: 60px;
    height: 24px;
  }

  /* Sidebar — becomes a true full-screen modal with a backdrop. The
   * 360px desktop width consumed 90vw on phones and left the map
   * invisible. Going full-width with a dimmed backdrop gives the
   * user a clear "you can close this to see the map" affordance. */
  #sidebar {
    width: 100vw;
    max-width: 100vw;
    border-left: none;
    box-shadow: none;
  }
  #sidebar-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.4);
    z-index: 49;  /* one below #sidebar (50) */
    animation: fade-in 0.18s ease-out;
  }
  #sidebar-backdrop[hidden] { display: none !important; }
  @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

  /* Modal — go full-bleed on phones. Desktop centres with 720px max;
   * on a 360px viewport there's no room for a centred card with
   * breathing space, and the form fields need the full width. */
  .modal-panel {
    width: 100vw;
    max-width: 100vw;
    height: 100vh;
    max-height: 100vh;
    border-radius: 0;
  }
  /* Form rows go single-column on phones — two side-by-side inputs
   * at 50% width each become unreadable below ~400px. */
  .form-row { grid-template-columns: 1fr; }
  .modal-body { padding: var(--space-4); }

  /* Footer — Chinese beian links stack and shrink on phones. */
  #beian-link {
    flex-wrap: wrap;
    justify-content: center;
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-2xs);
    gap: 4px;
  }
  .beian-sep { display: none; }
}

/* Very narrow (≤ 380px) — only hide "View:" label entirely and
   collapse add-plant to icon-only. The phase + view pills each get
   their own row at this width (flex: 1 1 100%) so the phase pill has
   plenty of room for the "Operating"/"Building"/"Planned" labels —
   no need to hide them. The old rule hid phase labels at ≤380px to
   squeeze the legend into a single row, but that layout is gone. */
@media (max-width: 380px) {
  .legend-label { font-size: 11px; }
}


/* ──────── User-bar (2026-07-15: 从 legacy style.css 提取, 兼容 auth.js
       动态注入的 user-bar-btn / user-bar-login) ──────── */
.user-bar {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: var(--space-3);
}
.user-bar-name {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  padding: var(--space-1) var(--space-2);
  background: var(--bg-subtle);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}
.user-bar-btn {
  font-size: var(--text-xs);
  padding: 5px 10px;
  border: 1px solid var(--border-strong);
  background: var(--bg-card);
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-primary);
  text-decoration: none;
  display: inline-block;
  transition: background-color 0.12s, border-color 0.12s;
}
.user-bar-btn:hover {
  background: var(--bg-hover);
  border-color: var(--text-muted);
}
.user-bar-btn.user-bar-login {
  background: var(--color-primary);
  color: white;
  border-color: var(--color-primary);
}
.user-bar-btn.user-bar-login:hover {
  background: var(--color-primary-hover);
  border-color: var(--color-primary-hover);
}
