  :root {
    /* Dark palette is the default. Light variant is applied by setting
       data-theme="light" on <html>; "auto" lets prefers-color-scheme
       drive it via the @media block at the bottom of this rule. The
       SPA's useTheme() hook owns persistence + the data attribute. */
    --bg-primary: #0a0a0f;
    --bg-secondary: #12121a;
    --bg-tertiary: #1a1a28;
    --bg-elevated: #222236;
    --border: #2a2a42;
    --border-hover: #3a3a5c;
    --text-primary: #e8e8f0;
    --text-secondary: #9898b0;
    --text-muted: #666680;
    --accent: #6c5ce7;
    --accent-hover: #7d6ff0;
    --accent-glow: rgba(108, 92, 231, 0.2);
    --success: #2ed573;
    --warning: #ffa502;
    --danger: #ff4757;
    --radius: 8px;
    --radius-lg: 12px;
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "DM Sans", system-ui, sans-serif;
    --font-mono: "JetBrains Mono", ui-monospace, monospace;
  }

  /* Explicit light theme. Used when the user picks "Light" in
     Settings → General, or when they pick "Auto" and the OS reports
     light mode (see @media block below). */
  html[data-theme="light"] {
    --bg-primary: #ffffff;
    --bg-secondary: #f5f5fa;
    --bg-tertiary: #ebebf2;
    --bg-elevated: #ffffff;
    --border: #d8d8e2;
    --border-hover: #b8b8c8;
    --text-primary: #1a1a28;
    --text-secondary: #4a4a60;
    --text-muted: #8a8aa0;
    --accent: #6c5ce7;
    --accent-hover: #5a4ad8;
    --accent-glow: rgba(108, 92, 231, 0.12);
  }
  /* "Auto" mode = no data-theme attribute set; defer to OS. The
     dark default in :root already matches OS-dark, so we only need
     to override when OS-light is active and the user hasn't pinned
     a theme explicitly. */
  @media (prefers-color-scheme: light) {
    html[data-theme="auto"] {
      --bg-primary: #ffffff;
      --bg-secondary: #f5f5fa;
      --bg-tertiary: #ebebf2;
      --bg-elevated: #ffffff;
      --border: #d8d8e2;
      --border-hover: #b8b8c8;
      --text-primary: #1a1a28;
      --text-secondary: #4a4a60;
      --text-muted: #8a8aa0;
      --accent-glow: rgba(108, 92, 231, 0.12);
    }
  }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  /* Lock the viewport: only inner panes scroll. The whole-page scroll
     pattern is the wrong default for an editor app — it makes the
     topbar drift, the layout gain a horizontal scrollbar when any
     pane overflows, and the PDF canvas no longer feels anchored. */
  html, body { height: 100%; overflow: hidden; }
  body {
    font-family: var(--font-sans);
    background: var(--bg-secondary);
    color: var(--text-primary);
    -webkit-font-smoothing: antialiased;
  }
  a { color: var(--accent); text-decoration: none; }
  a:hover { color: var(--accent-hover); }
  ::-webkit-scrollbar { width: 6px; }
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
  ::-webkit-scrollbar-thumb:hover { background: var(--border-hover); }

  /* ---------- shared ---------- */
  .btn {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 9px 16px; border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--bg-tertiary); color: var(--text-primary);
    font: 600 13px var(--font-sans); cursor: pointer;
    transition: all 0.15s;
  }
  .btn:hover { border-color: var(--border-hover); background: var(--bg-elevated); }
  .btn-primary {
    background: var(--accent); border-color: var(--accent); color: white;
  }
  .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
  .btn-danger { color: var(--danger); border-color: var(--border); }
  .btn-danger:hover { background: var(--danger); color: white; border-color: var(--danger); }
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }

  .input, .select {
    width: 100%; padding: 8px 10px; border: 1px solid var(--border);
    border-radius: var(--radius); background: var(--bg-primary); color: var(--text-primary);
    font: 13px var(--font-sans); transition: border-color 0.15s;
  }
  .input:focus, .select:focus { outline: none; border-color: var(--accent); }
  .label { display: block; font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.04em; }
  /* Horizontal textarea resize blows out parent layout (sidebars, modals,
     forms). Vertical-only is what users actually want for multi-line text. */
  textarea { resize: vertical; }

  /* Custom checkbox + radio styling. Replaces the OS-default chrome
     so they read on the dark theme and match the rest of the form
     controls. Built with `appearance: none` + ::after pseudo-element
     for the glyph. accent-color falls back to the default look on
     browsers that ignore appearance:none (very old). */
  input[type="checkbox"], input[type="radio"] {
    appearance: none; -webkit-appearance: none;
    width: 16px; height: 16px;
    margin: 0; padding: 0;
    background: var(--bg-primary);
    border: 1px solid var(--border);
    border-radius: 3px;
    cursor: pointer;
    position: relative;
    flex-shrink: 0;
    transition: border-color 0.12s, background 0.12s;
    accent-color: var(--accent);
    vertical-align: middle;
  }
  input[type="radio"] { border-radius: 50%; }
  input[type="checkbox"]:hover, input[type="radio"]:hover {
    border-color: var(--border-hover);
  }
  input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px var(--accent-glow);
    border-color: var(--accent);
  }
  input[type="checkbox"]:checked,
  input[type="radio"]:checked {
    background: var(--accent);
    border-color: var(--accent);
  }
  input[type="checkbox"]:checked::after {
    content: ''; position: absolute;
    left: 5px; top: 1px;
    width: 4px; height: 9px;
    border: solid white;
    border-width: 0 2px 2px 0;
    transform: rotate(45deg);
  }
  input[type="radio"]:checked::after {
    content: ''; position: absolute;
    left: 50%; top: 50%;
    width: 6px; height: 6px;
    margin-left: -3px; margin-top: -3px;
    background: white; border-radius: 50%;
  }
  input[type="checkbox"]:disabled, input[type="radio"]:disabled {
    opacity: 0.4; cursor: not-allowed;
  }

  /* Custom select chevron — kills the native arrow, swaps in our
     own SVG so it inherits the muted-text color. */
  .select, select.input {
    appearance: none; -webkit-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239898b0' stroke-width='2.5' stroke-linecap='round'><polyline points='6 9 12 15 18 9'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
    padding-right: 32px;
  }

  .card {
    background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-lg);
    padding: 20px;
  }
  .card-title { font-size: 16px; font-weight: 700; margin-bottom: 16px; }

  .toast-container {
    position: fixed; bottom: 24px; right: 24px; display: flex; flex-direction: column; gap: 8px; z-index: 9999;
  }
  .toast {
    padding: 12px 16px; border-radius: var(--radius); border: 1px solid var(--border);
    background: var(--bg-elevated); font-size: 13px; max-width: 360px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.4);
  }
  .toast.success { border-color: var(--success); }
  .toast.error { border-color: var(--danger); }

  /* ---------- auth shell ---------- */
  .auth-page {
    min-height: 100vh; display: flex; align-items: center; justify-content: center;
    background: radial-gradient(ellipse at top, var(--accent-glow), transparent 60%), var(--bg-secondary);
    padding: 24px;
  }
  .auth-card {
    width: 100%; max-width: 400px;
    background: var(--bg-tertiary); border: 1px solid var(--border);
    border-radius: var(--radius-lg); padding: 32px;
  }
  .auth-card h1 { font-size: 22px; margin-bottom: 4px; }
  .auth-card .sub { font-size: 13px; color: var(--text-secondary); margin-bottom: 24px; }
  .oauth-row { display: flex; flex-direction: column; gap: 8px; }
  .divider { display: flex; align-items: center; gap: 12px; margin: 20px 0; color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
  .divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
  .form-row { display: flex; flex-direction: column; gap: 12px; }
  /* Inside a .card, form-row uses tighter spacing + labelled fields. The
     auth screens still use the loose default above. */
  .card .form-row { gap: 6px; margin-bottom: 16px; }
  .card .form-row:last-child { margin-bottom: 0; }
  .card .form-row label {
    font-size: 12px; font-weight: 500; color: var(--text-secondary);
    text-transform: uppercase; letter-spacing: 0.04em;
  }
  .form-hint { font-size: 11px; color: var(--text-muted); margin-top: 2px; }

  /* Schema tab — Inline/Saved two-segment toggle. Mirrors the
     original PDF Microservice Builder's panel header. */
  .schema-mode-toggle {
    display: inline-flex;
    background: var(--bg-elevated);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
  }
  .schema-mode-btn {
    border: none; background: transparent; color: var(--text-secondary);
    padding: 8px 18px; font: 600 12px var(--font-sans); cursor: pointer;
    transition: all 0.15s;
  }
  .schema-mode-btn:hover { color: var(--text-primary); }
  .schema-mode-btn.active { background: var(--accent); color: white; }
  .schema-mode-btn + .schema-mode-btn { border-left: 1px solid var(--border); }

  /* Read-only JSON preview block — used for the linked-schema view +
     the auto-generated sample request body. */
  .schema-preview {
    margin: 0;
    padding: 12px;
    background: var(--bg-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-family: var(--font-mono); font-size: 12px;
    color: var(--text-secondary);
    max-height: 280px; overflow: auto;
    white-space: pre-wrap; word-break: break-word;
  }
  .auth-switch { text-align: center; margin-top: 16px; font-size: 13px; color: var(--text-secondary); }

  /* ---------- app shell ---------- */
  /* `height: 100vh` (not min-height) + overflow:hidden caps the page at
     the viewport so the topbar can't drift, no body scrollbar appears,
     and overflow handling becomes the panes' problem. min-width:0 on
     flex descendants stops a wide PDF from forcing horizontal page
     scroll — they shrink instead and the canvas-scroll pane handles
     overflow internally. */
  .app {
    display: flex; flex-direction: column;
    height: 100vh; max-height: 100vh; overflow: hidden;
  }
  .topbar {
    display: flex; align-items: center; justify-content: space-between;
    padding: 0 24px; height: 56px; flex-shrink: 0;
    background: var(--bg-secondary); border-bottom: 1px solid var(--border);
    /* Above the selected field-overlay stack (z:10) so the avatar
       dropdown, workspace dropdown, and any other topbar popovers can't
       be punched through by a placed field that happens to render at
       the same z-level. */
    position: relative; z-index: 50;
  }
  .topbar-logo { display: flex; align-items: center; gap: 10px; font-weight: 700; font-size: 16px; letter-spacing: -0.02em; }
  .topbar-logo img { width: 24px; height: 24px; }
  .topbar-actions { display: flex; gap: 10px; align-items: center; }

  .tenant-pill {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 14px; background: var(--accent-glow); color: var(--accent);
    border-radius: 999px; font-size: 13px; font-weight: 600; cursor: pointer;
    border: 1px solid transparent; user-select: none;
    /* Long workspace names should ellipsize, not wrap to a second line.
       max-width is responsive so the pill never crowds the topbar logo
       or the avatar pill on the right. The inner name span carries the
       overflow rules; the chevron stays visible at the end. */
    max-width: min(60vw, 320px);
    white-space: nowrap; overflow: hidden;
  }
  .tenant-pill > .tenant-pill-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    min-width: 0;
  }
  .tenant-pill:hover { border-color: var(--accent); }
  .tenant-dropdown {
    position: absolute; top: 56px; left: 50%; transform: translateX(-50%);
    min-width: 240px; background: var(--bg-elevated); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: 0 12px 32px rgba(0,0,0,0.5);
    padding: 6px; z-index: 100;
  }
  .tenant-item {
    display: flex; align-items: center; justify-content: space-between;
    padding: 10px 12px; border-radius: 6px; cursor: pointer; font-size: 13px;
  }
  .tenant-item:hover { background: var(--bg-tertiary); }
  .tenant-item.active { background: var(--accent-glow); color: var(--accent); font-weight: 600; }
  .tenant-item .role { font-size: 11px; color: var(--text-muted); }

  .layout { display: flex; flex: 1; min-height: 0; min-width: 0; overflow: hidden; }
  .sidebar {
    width: 220px; background: var(--bg-secondary); border-right: 1px solid var(--border);
    padding: 16px 8px; flex-shrink: 0;
  }
  .nav-item {
    display: flex; align-items: center; gap: 10px;
    padding: 10px 14px; border-radius: var(--radius); cursor: pointer;
    color: var(--text-secondary); font-size: 13px; font-weight: 500;
    margin-bottom: 2px;
  }
  .nav-item:hover { background: var(--bg-tertiary); color: var(--text-primary); }
  .nav-item.active { background: var(--accent-glow); color: var(--accent); }

  /* ---------- Settings shell (matches the Backup-page screenshot) ---------- */
  .settings-layout { display: flex; flex: 1; min-height: 0; }
  .settings-sidebar {
    width: 240px; background: var(--bg-secondary); border-right: 1px solid var(--border);
    padding: 28px 18px; flex-shrink: 0;
    display: flex; flex-direction: column;
  }
  .settings-sidebar .section-label {
    font-size: 11px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.08em; color: var(--text-muted); margin: 0 6px 12px;
  }
  .settings-sidebar .nav-item { padding: 9px 12px; font-size: 14px; margin-bottom: 1px; }
  .settings-sidebar .sidebar-divider {
    height: 1px; background: var(--border); margin: 16px 6px;
  }
  .settings-back {
    display: block; padding: 9px 12px; color: var(--text-secondary); font-size: 13px;
    cursor: pointer; border-radius: var(--radius);
  }
  .settings-back:hover { color: var(--text-primary); background: var(--bg-tertiary); }
  .settings-main { flex: 1; padding: 40px 56px; overflow-y: auto; max-width: 760px; }
  .settings-main-mobile-header { display: none; }
  @media (max-width: 768px) {
    .settings-layout.is-mobile { flex-direction: column; }
    .settings-layout.is-mobile .settings-sidebar {
      width: 100%; flex: 1; min-height: 0; border-right: none; padding: 16px 14px;
      overflow-y: auto;
    }
    .settings-layout.is-mobile .settings-main { padding: 0; max-width: none; }
    .settings-main-mobile-header {
      display: flex; align-items: center; gap: 12px;
      padding: 12px 14px; border-bottom: 1px solid var(--border);
      background: var(--bg-secondary);
      position: sticky; top: 0; z-index: 5;
    }
    .settings-main-back {
      display: inline-flex; align-items: center; gap: 4px;
      background: transparent; border: none; color: var(--text-secondary);
      font: 500 13px var(--font-sans); cursor: pointer; padding: 4px 6px;
      border-radius: var(--radius);
    }
    .settings-main-back:hover { color: var(--text-primary); background: var(--bg-tertiary); }
    .settings-main-mobile-title {
      font-weight: 600; font-size: 14px; color: var(--text-primary);
    }
    /* Each settings page already has its own padding container; on
       mobile we want the page-level header to flow under the back bar
       without the desktop margin pulling it off-screen. A small top
       pad keeps the page title from kissing the sticky back-bar
       border (original report: titles read as crammed against the top
       of the content area). */
    .settings-layout.is-mobile .settings-main > *:not(.settings-main-mobile-header) {
      padding-left: 16px; padding-right: 16px;
    }
    .settings-layout.is-mobile .settings-main > .settings-main-mobile-header + * {
      padding-top: 16px;
    }
  }
  .settings-section-label {
    font-size: 11px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.08em; color: var(--text-muted); margin: 24px 0 12px;
  }
  /* ---------- Publish button + status pill ---------- */
  .publish-btn {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 8px 16px; border-radius: var(--radius);
    background: var(--accent); color: #fff;
    font-size: 13px; font-weight: 600; border: none; cursor: pointer;
  }
  .publish-btn:hover { filter: brightness(1.1); }
  .publish-btn:disabled { opacity: 0.5; cursor: not-allowed; }
  .publish-status-dot {
    width: 8px; height: 8px; border-radius: 50%; display: inline-block;
    background: var(--text-muted);
  }
  .publish-status-dot.clean { background: #22c55e; }
  .publish-status-dot.dirty { background: #f59e0b; }
  .publish-status-dot.failed { background: #ef4444; }
  .publish-state-banner {
    display: flex; align-items: center; gap: 8px;
    padding: 10px 14px; border-radius: var(--radius);
    background: var(--bg-elevated); margin-bottom: 16px;
    font-size: 13px;
  }
  .publish-state-drift { background: rgba(245, 158, 11, 0.12); }
  .publish-state-failed { background: rgba(239, 68, 68, 0.12); }
  .publish-log-buffer {
    background: #0c0e10; color: #d8dde3;
    padding: 12px; border-radius: var(--radius);
    font-family: ui-monospace, SF Mono, monospace; font-size: 11.5px;
    max-height: 220px; overflow-y: auto;
    white-space: pre-wrap; word-break: break-all; margin: 0;
  }
  .publish-status-dot.building {
    background: var(--accent);
    animation: publishPulse 1.2s ease-in-out infinite;
  }
  @keyframes publishPulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }

  /* ---------- Avatar dropdown ---------- */
  .avatar-btn {
    width: 36px; height: 36px; border-radius: 50%; cursor: pointer;
    background: var(--bg-elevated); border: 1px solid var(--border);
    display: flex; align-items: center; justify-content: center;
    font-size: 13px; font-weight: 600; color: var(--text-primary);
    user-select: none;
    /* Was overflow:hidden so the avatar image clips to a circle. The
       notif dot needs to sit OUTSIDE the circle's edge (bottom-right
       corner) so we drop overflow here and re-clip the <img> itself. */
    position: relative;
  }
  .avatar-btn img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
  /* Small yellow dot pinned to the avatar's bottom-right corner.
     White ring punches it cleanly off the avatar's background regardless
     of theme. Yellow = "you have something to look at" — softer than
     red, which we reserve for errors / over-cap / suspension. */
  .avatar-notif-dot {
    position: absolute;
    bottom: -1px; right: -1px;
    width: 10px; height: 10px;
    border-radius: 50%;
    background: #f5b400;
    box-shadow: 0 0 0 2px var(--bg-primary);
  }
  /* Inline numeric badge used in dropdown rows ("Get help [3]"). Same
     yellow as the avatar dot so the two affordances visually correlate. */
  .notif-badge {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 18px; height: 18px;
    padding: 0 6px;
    border-radius: 9px;
    background: #f5b400;
    color: #1a1a1a;
    font-size: 11px; font-weight: 700; line-height: 1;
  }
  .avatar-dropdown {
    position: absolute; right: 0; top: calc(100% + 8px); z-index: 200;
    min-width: 220px;
    background: var(--bg-secondary); border: 1px solid var(--border);
    border-radius: var(--radius-lg); padding: 6px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.4);
  }
  .avatar-dropdown-header { padding: 10px 12px; font-size: 13px; }
  /* Active organisation — the operating context. Slightly bigger than the
     user identity below so it reads as the heading of this header block. */
  .avatar-dropdown-header .org {
    font-size: 15px; font-weight: 700; color: var(--accent);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .avatar-dropdown-header .name { font-weight: 600; color: var(--text-primary); margin-top: 2px; }
  .avatar-dropdown-header .email { color: var(--text-muted); font-size: 12px; margin-top: 2px; }
  .avatar-dropdown-divider { height: 1px; background: var(--border); margin: 6px 0; }
  .avatar-dropdown-item {
    padding: 9px 12px; font-size: 13px; cursor: pointer;
    border-radius: var(--radius); color: var(--text-primary);
    display: flex; justify-content: space-between; align-items: center;
  }
  .avatar-dropdown-item:hover { background: var(--bg-tertiary); }
  .avatar-dropdown-item .hint { font-size: 11px; color: var(--text-muted); }
  /* Destructive entry (Sign out). Red text + soft red wash on hover so
     the affordance feels different from the neutral menu items. */
  .avatar-dropdown-item-danger { color: var(--danger); }
  .avatar-dropdown-item-danger:hover { background: rgba(255, 71, 87, 0.1); }

  /* Profile-page avatar upload — circular preview with the user's
     initial as a fallback, swapped to the uploaded image when set.
     Doubles as the live preview during pick-then-upload. */
  .avatar-upload-preview {
    width: 72px; height: 72px; flex-shrink: 0;
    border-radius: 50%;
    background: var(--accent);
    color: white;
    display: flex; align-items: center; justify-content: center;
    font: 600 28px var(--font-sans);
    overflow: hidden;
    border: 2px solid var(--border);
  }
  .avatar-upload-preview img {
    width: 100%; height: 100%; object-fit: cover;
  }

  .main { flex: 1; padding: 32px; max-width: 1100px; }
  .page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
  .page-title { font-size: 22px; font-weight: 700; }
  .page-subtitle { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }

  .table { width: 100%; border-collapse: collapse; }
  .table th, .table td { text-align: left; padding: 12px 16px; font-size: 13px; border-bottom: 1px solid var(--border); }
  .table th { font-size: 11px; text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.04em; }
  .table tr:last-child td { border-bottom: none; }

  .badge {
    display: inline-block; padding: 3px 10px; border-radius: 999px;
    font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
  }
  .badge-free { background: var(--bg-elevated); color: var(--text-secondary); }
  .badge-advanced { background: rgba(45, 125, 240, 0.15); color: #2d7df0; }
  .badge-pro { background: var(--accent-glow); color: var(--accent); }
  .badge-enterprise { background: rgba(255, 165, 2, 0.15); color: var(--warning); }

  /* Card / page section locked behind a tier. Same padding + border as a
     normal settings card; gets a subtle tinted background so the lock is
     visible without screaming. The inner controls stay visible (with
     pointer-events: none + opacity) so the operator can see what they'd
     be unlocking. */
  .tier-locked {
    position: relative;
    border: 1px dashed var(--border);
    border-radius: var(--radius-lg);
    padding: 16px;
    background: rgba(108, 92, 231, 0.04);
  }
  .tier-locked-inner { opacity: 0.45; pointer-events: none; user-select: none; }
  .tier-locked-cta {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    margin-top: 12px; padding-top: 12px;
    border-top: 1px dashed var(--border);
    font-size: 13px; color: var(--text-secondary);
  }
  .tier-locked-cta .btn { margin-left: auto; }

  .empty {
    padding: 60px 20px; text-align: center; color: var(--text-muted);
    border: 1px dashed var(--border); border-radius: var(--radius-lg);
  }

  .modal-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,0.6);
    display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 24px;
  }
  .modal {
    background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-lg);
    padding: 28px; width: 100%; max-width: 460px;
  }
  .modal h2 { font-size: 18px; margin-bottom: 16px; }
  .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }

  .key-display {
    font-family: var(--font-mono); font-size: 12px; padding: 12px;
    background: var(--bg-primary); border-radius: var(--radius); border: 1px solid var(--border);
    word-break: break-all; margin: 12px 0;
  }

  /* ---------- editor view ---------- */
  /* TemplatesPage 3-pane: browser left | editor (canvas + side) right.
     min-height:0 + min-width:0 keep the children from forcing the
     parent to grow when their content is wider/taller than the pane. */
  .templates-3pane {
    display: flex; flex: 1; min-height: 0; min-width: 0; overflow: hidden;
    margin: 0; /* override .main padding */
  }
  .templates-3pane-editor {
    flex: 1; display: flex; flex-direction: column; min-width: 0;
  }
  /* Secondary pages reached from the avatar dropdown (api keys, jobs,
     billing, license, account, admin-*). With the left sidebar gone,
     these need to center themselves in the viewport instead of relying
     on flex from a parent .layout. */
  .main-secondary {
    flex: 1;
    padding: 32px;
    max-width: 1100px;
    margin: 0 auto;
    width: 100%;
    overflow-y: auto;
  }

  /* Template browser sidebar — ported to match the original PDF
     Microservice Builder layout: section header with icon-only folder
     button + add button, search input, then a tree of folders (caret +
     folder icon + name + count badge) with indented template rows. */
  .template-browser {
    width: 280px; flex-shrink: 0;
    background: var(--bg-secondary); border-right: 1px solid var(--border);
    display: flex; flex-direction: column; min-height: 0;
    /* Belt-and-suspenders against future overflow regressions: any
       child that goes 100%+margin (a classic width:100% trap) would
       blow out the column width without this. */
    overflow: hidden;
  }
  .template-browser-header {
    display: flex; align-items: center; gap: 8px;
    padding: 14px 14px 8px;
  }
  .template-browser-header .section-label {
    font-size: 11px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.06em; color: var(--text-muted);
  }
  /* `.input`'s `width: 100%` + horizontal margin would overflow the
     280px sidebar by 28px (border-box width is 100% of parent, margins
     stack on top). calc() pulls the inputs back inside the column.
     Same shape as the original PDF Microservice Builder's
     `sidebar-search` rule. */
  .template-browser-search {
    width: calc(100% - 28px);
    margin: 0 14px 12px; padding: 8px 10px; font-size: 13px;
  }
  .template-browser-list {
    flex: 1; overflow-y: auto; padding: 0 8px 16px;
  }

  /* Template-card layout — ported from the original. Each entry is a
     bordered card with name + N-fields badge on top, originalName +
     active indicator + duplicate button on bottom. Hover lifts to
     bg-elevated; active gets the accent border + a 1px ring. */
  .sidebar-folder {
    margin-bottom: 8px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    background: var(--bg-secondary);
    transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
  }
  .folder-header {
    display: flex; align-items: center; justify-content: space-between;
    padding: 8px 10px;
    cursor: pointer; user-select: none;
    font-size: 12px; font-weight: 600; color: var(--text-secondary);
  }
  .folder-header:hover { color: var(--text-primary); }
  .folder-header-left {
    display: flex; align-items: center; gap: 6px;
    overflow: hidden; flex: 1; min-width: 0;
  }
  .folder-header .folder-name {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .folder-header .folder-count {
    font-size: 10px; color: var(--text-muted);
    background: var(--bg-elevated); padding: 1px 7px; border-radius: 999px;
    flex-shrink: 0;
  }
  /* Delete control sits next to the count badge. Default is muted +
     transparent so the folder header reads as quiet; on hover the
     icon flashes to danger to telegraph the destructive action. The
     button is always rendered (no opacity-0 → 1 transition) so
     keyboard users can tab to it the same as anywhere else. */
  .folder-header .folder-delete {
    flex-shrink: 0;
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; padding: 0;
    background: transparent; border: none;
    color: var(--text-muted); cursor: pointer;
    border-radius: 4px; transition: color 0.1s, background 0.1s;
  }
  .folder-header .folder-delete:hover {
    color: var(--danger); background: rgba(220, 38, 38, 0.08);
  }
  .folder-body { padding: 0 6px 6px; }
  .folder-body .template-card { margin-bottom: 4px; }
  .folder-empty {
    padding: 10px;
    text-align: center;
    font-size: 11px; color: var(--text-muted);
    border: 1px dashed var(--border); border-radius: var(--radius);
  }

  .template-card {
    padding: 10px 10px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    margin-bottom: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
    background: var(--bg-tertiary);
    overflow: hidden;
  }
  /* Touch reorder visuals: source row dims, the cloned ghost follows
     the finger, and any folder under the finger gets a highlighted ring. */
  .template-card-dragging { opacity: 0.4; }
  .template-card-ghost {
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
    border-color: var(--accent) !important;
  }
  .folder-drop-hover { background: var(--accent-glow); border-radius: var(--radius); outline: 2px solid var(--accent); outline-offset: -2px; }
  .template-card:hover {
    border-color: var(--border-hover);
    background: var(--bg-elevated);
  }
  .template-card.active {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
  }
  .template-card-name {
    font-size: 13px; font-weight: 600; margin-bottom: 2px;
    display: flex; align-items: start; justify-content: space-between;
    gap: 8px; min-width: 0;
  }
  .template-card-name > span:first-child {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .template-card-badge {
    flex-shrink: 0;
    font-size: 10px; font-weight: 600;
    background: var(--accent-glow); color: var(--accent);
    padding: 2px 7px; border-radius: 999px; line-height: 1.4;
  }
  .template-card-meta {
    font-size: 11px;
    color: var(--text-muted);
    font-family: var(--font-mono);
    min-width: 0;
    display: flex; align-items: center; justify-content: space-between; gap: 6px;
  }
  .template-card-meta > span:first-child {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .template-card-meta > .template-card-actions {
    flex-shrink: 0; display: flex; align-items: center; gap: 4px;
  }
  /* Active indicator: green check when enabled, muted when disabled. */
  .template-card-active {
    width: 22px; height: 22px;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--success);
  }
  .template-card-active.disabled { color: var(--text-muted); opacity: 0.5; }
  /* Copy / duplicate button. 0.5 opacity at rest, full on hover. */
  .template-card-copy {
    width: 22px; height: 22px; padding: 0;
    display: inline-flex; align-items: center; justify-content: center;
    background: transparent; border: none; cursor: pointer;
    color: var(--text-muted); opacity: 0.5;
    border-radius: 4px;
  }
  .template-card-copy:hover {
    opacity: 1; color: var(--text-primary);
    background: var(--bg-elevated);
  }

  /* Icon-only button variant: square 28×28, no padding, just centers
     an SVG. Used in sidebar headers and row actions where the label
     would be redundant. */
  .btn.btn-icon {
    width: 28px; min-width: 28px; height: 28px;
    padding: 0; justify-content: center;
  }
  .btn-sm { padding: 6px 12px; font-size: 12px; }
  .btn-sm.btn-icon { width: 28px; min-width: 28px; height: 28px; padding: 0; }

  /* Properties-panel toolbar: a single row of icon buttons that sits
     above the field-type banner / name input. The buttons share the
     panel's full width via flex with even gaps; the active lock state
     paints the toggle in the accent colour so the user can spot a
     locked field at a glance without reading the title attribute. */
  .prop-toolbar {
    display: flex; gap: 6px;
    margin-bottom: 12px; padding-bottom: 12px;
    border-bottom: 1px solid var(--border);
  }
  .prop-toolbar .btn-icon { flex: 1 1 auto; }
  /* Active "lock on" state: accent border + accent foreground so the
     icon reads as "engaged" without using a different size or motion. */
  .btn.is-locked-on,
  .btn.is-locked-on:hover {
    border-color: var(--accent);
    color: var(--accent);
    background: rgba(108, 92, 231, 0.08);
  }

  /* Slightly opaque disabled appearance for form controls inside the
     properties panel. The browser's default disabled styling is
     usually a flat 1.0 alpha that's indistinguishable from enabled —
     this gives it a quick visual cue that the row is intentionally
     non-interactive. Buttons keep their existing .btn:disabled rule
     (defined further up) and aren't double-faded. */
  .prop-group .input:disabled,
  .prop-group .select:disabled,
  .prop-group textarea.input:disabled,
  .prop-group input[type="checkbox"]:disabled,
  .prop-group input[type="radio"]:disabled,
  .prop-group input[type="color"]:disabled {
    opacity: 0.55; cursor: not-allowed;
  }
  /* Whole row variant — when the wrapping .prop-group is itself
     marked locked, dim labels and helper text along with the input so
     the visual treatment reads as "this group is grayed out", not
     "the label is bright and the input is faded". */
  .prop-group[data-locked-tooltip] .label,
  .prop-group[data-locked-tooltip] .checkbox-row { opacity: 0.6; }

  /* Transform-code syntax-error feedback. Border swap-in on the
     textarea + an amber warning icon next to the label. The
     PropertiesPanel sets `has-syntax-error` after a blur-time
     `new AsyncFunction(...)` check throws SyntaxError — see
     validateTransformSyntax in the same component. */
  .input.has-syntax-error,
  .input.has-syntax-error:focus,
  textarea.input.has-syntax-error,
  textarea.input.has-syntax-error:focus {
    border-color: var(--danger);
    box-shadow: 0 0 0 1px var(--danger);
  }
  .transform-syntax-warning {
    display: inline-flex; align-items: center; gap: 4px;
    color: var(--warning, #f5b400);
    margin-left: 6px;
    font-size: 11px;
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
  }

  /* Custom hover tooltip on locked-disabled prop groups. Replaces the
     browser's native title= tooltip — which has a long delay (~500ms
     before reveal, varies by OS), draws in the browser chrome (not
     themed), and on disabled inputs doesn't fire at all in Chrome.
     The CSS-only pattern: the prop-group is the hover target (since
     it's a div, not the disabled input), and ::after paints the
     tooltip on top of it. pointer-events:none on the tooltip itself
     keeps it from intercepting clicks on whatever's behind. */
  .prop-group[data-locked-tooltip] { position: relative; }
  .prop-group[data-locked-tooltip]:hover::after {
    content: attr(data-locked-tooltip);
    position: absolute;
    bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%);
    background: var(--bg-elevated);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 6px 10px;
    font-size: 11px; font-weight: 500;
    white-space: nowrap;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
    pointer-events: none;
    z-index: 20;
  }
  /* Tiny triangle pointing down from the tooltip to the row it
     describes. Implemented as a clip-path on a ::before pseudo so the
     border on the tooltip body itself stays clean. */
  .prop-group[data-locked-tooltip]:hover::before {
    content: '';
    position: absolute;
    bottom: 100%;
    left: 50%; transform: translate(-50%, 1px);
    width: 10px; height: 6px;
    background: var(--border);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
    pointer-events: none;
    z-index: 21;
  }

  .template-settings-meta {
    background: var(--bg-secondary); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 12px;
    font-size: 12px; line-height: 1.7;
    font-family: var(--font-mono, ui-monospace, monospace);
    color: var(--text-secondary);
  }
  .template-settings-meta .meta-label {
    color: var(--text-muted); margin-right: 4px;
  }
  .card-success { border-color: rgba(34,197,94,0.5); background: rgba(34,197,94,0.06); }
  .card-warn    { border-color: rgba(245,158,11,0.5); background: rgba(245,158,11,0.06); }

  /* Status toggle on the template Settings tab. Fixed height + flex
     row so swapping between "Active / Available." and
     "Inactive / Not available." doesn't reflow the surrounding form.
     The hint truncates with ellipsis on tight viewports rather than
     wrapping into a second line. */
  .template-status-card {
    padding: 10px 12px;
    display: flex; align-items: center; gap: 10px;
    cursor: pointer;
    height: 44px; min-height: 44px;
    overflow: hidden;
    transition: background 0.15s, border-color 0.15s;
  }
  .template-status-card:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.18);
  }
  .template-status-card strong {
    flex-shrink: 0; font-size: 13px;
  }
  .template-status-hint {
    flex: 1; min-width: 0;
    font-size: 12px; color: var(--text-secondary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }

  .editor-layout { display: flex; flex: 1; min-height: 0; }
  .editor-canvas-area {
    flex: 1; display: flex; flex-direction: column;
    background: var(--bg-primary); border-right: 1px solid var(--border);
    min-width: 0;
  }
  .editor-toolbar {
    display: flex; align-items: center; gap: 12px;
    padding: 10px 16px; background: var(--bg-secondary);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
  }
  .editor-toolbar .spacer { flex: 1; }
  .page-nav { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-secondary); }
  .page-nav button { padding: 4px 10px; border: 1px solid var(--border); background: var(--bg-tertiary); border-radius: 6px; color: var(--text-primary); cursor: pointer; display: inline-flex; align-items: center; gap: 6px; }
  .page-nav button:disabled { opacity: 0.4; cursor: not-allowed; }
  /* Active state for the toggle buttons (Place field / Hide overlays).
     Same rectangle as the other page-nav buttons, just tinted with the
     accent palette so it's clearly the engaged state — matches the
     outlined-pill look the zoom + pagination groups already use,
     instead of the chunky filled .btn-primary square the toolbar
     previously had next to them. */
  .page-nav button.is-active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); }
  /* Pagination control absorbs the horizontal space freed by removing
     the zoom buttons. Bigger chevron hit-targets and a longer page
     label make it read as the primary toolbar group. */
  .page-nav-pages button { padding: 4px 18px; min-width: 44px; font-size: 14px; }
  .page-nav-pages .page-label { padding: 0 10px; }
  .canvas-scroll {
    flex: 1; overflow: auto; padding: 24px;
    display: flex; align-items: flex-start; justify-content: center;
    /* pan-x pan-y lets one-finger drags scroll the PDF as usual but
       yields pinch (multi-finger) gestures to our pointer handlers
       instead of triggering the browser's native page zoom. Without
       this the browser eats the second pointer before our React
       handlers see it. */
    touch-action: pan-x pan-y;
  }
  .pdf-wrapper {
    position: relative; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
    background: white;
  }
  .pdf-wrapper.placing { cursor: crosshair; }
  .pdf-wrapper canvas { display: block; }
  /* Two distinct visual languages — never mix:
       Floating (manually-placed) → green (#2ed573).
       Form-detected (PDF AcroForm) → purple (var(--accent)).
     Selected uses a brighter shade of the same family (no cross-color
     swap) so the user always knows whether they're looking at a
     placed or detected field. */
  .field-overlay {
    position: absolute; box-sizing: border-box;
    border: 2px solid rgba(46, 213, 115, 0.7);
    background: rgba(46, 213, 115, 0.10);
    color: #1a3a1a;
    font-size: 11px;
    display: flex; align-items: center; padding: 0 6px;
    cursor: move; user-select: none;
    border-radius: 3px;
    /* 10×10px floor — the overlay still reflects the field's true size
       at every zoom level, but stays grabbable. The previous 60×22px
       floor inflated small fields into giant rectangles when zoomed
       out (or whenever the operator legitimately placed a tiny shape).
       The label inside has overflow:hidden + text-overflow:ellipsis so
       it clips cleanly when the box is too small to read it. */
    min-width: 10px; min-height: 10px;
    transition: border-color 0.1s, background 0.1s;
    /* On touch devices, the browser would otherwise interpret a drag on
       the field as a page scroll. `touch-action: none` hands the gesture
       to our pointermove handler instead. */
    touch-action: none;
  }
  .field-overlay:hover {
    border-color: var(--success);
    background: rgba(46, 213, 115, 0.18);
  }
  /* Locked: dashed border + non-drag cursor signal that this overlay
     is anchored. The drag/resize handlers and the canvas delete × are
     also disabled at the React layer so the cursor change is the only
     in-canvas affordance — the toolbar in the properties panel is
     where the user actually unlocks. */
  .field-overlay.is-locked { cursor: default; border-style: dashed; }
  .field-overlay.is-locked:hover { background: rgba(46, 213, 115, 0.14); }
  .field-overlay.selected {
    border-color: var(--success);
    background: rgba(46, 213, 115, 0.28);
    box-shadow: 0 0 0 2px rgba(46, 213, 115, 0.25);
    z-index: 10;
  }

  /* Form-detected: purple. Anchored to PDF widget rects, pointer
     cursor (no drag), label inside shows what'll fill the widget.
     unconfigured = dashed muted purple, configured = solid accent. */
  .field-overlay.form-detected {
    cursor: pointer;
    border-color: rgba(108, 92, 231, 0.55);
    background: rgba(108, 92, 231, 0.08);
    color: #2a1f5a;
  }
  .field-overlay.form-detected.unconfigured {
    border-style: dashed;
    border-color: rgba(108, 92, 231, 0.45);
    background: rgba(108, 92, 231, 0.05);
  }
  .field-overlay.form-detected:hover {
    border-color: var(--accent);
    background: rgba(108, 92, 231, 0.14);
  }
  .field-overlay.form-detected.configured {
    border-style: solid;
    border-color: var(--accent);
    background: rgba(108, 92, 231, 0.14);
  }
  .field-overlay.form-detected.selected {
    border-color: var(--accent);
    background: rgba(108, 92, 231, 0.28);
    box-shadow: 0 0 0 2px rgba(108, 92, 231, 0.25);
    color: #1a1a2e;
  }
  /* Pre-checked state: source PDF had this checkbox / radio pre-set.
     Stronger tinted background + solid border so the overlay reads as
     "this one is on" at a glance. Compounds with .selected — a selected
     pre-checked field gets both a pre-check tint and the selection ring. */
  .field-overlay.form-detected.is-prechecked {
    background: rgba(108, 92, 231, 0.22);
    border-color: var(--accent);
    border-style: solid;
  }
  /* Precheck glyph (✓ or ●) for form-detected checkboxes/radios that
     the source PDF had marked. The parent .field-overlay already
     centres its children via flex (align-items:center +
     justify-content:center on the .form-detected variant), so the
     glyph just flows inline — no absolute positioning needed. Earlier
     versions corner-pinned it to share space with the field name
     label, but the label was removed from these overlays (squeezed
     text on tiny widgets read as a glitch), so the glyph gets the
     whole rect to itself now. */
  .field-overlay-precheck {
    font-size: 13px;
    line-height: 1;
    font-weight: 700;
    color: var(--accent);
    pointer-events: none;
    text-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
  }
  /* Dark theme: a small white-ish shadow ring around the glyph keeps it
     readable on the tinted overlay background regardless of canvas
     content underneath. */
  html[data-theme="dark"] .field-overlay-precheck,
  html:not([data-theme="light"]) .field-overlay-precheck {
    color: #d4caff;
    text-shadow: 0 0 3px rgba(10, 10, 15, 0.7);
  }
  /* Label inside a (non-checkbox, non-radio, non-image) overlay.
     Layout is icon | name | hint, all on one line. The hint uses
     flex: 1 1 0 + overflow: hidden so it gracefully shrinks away to
     zero width when the overlay isn't wide enough — that's how we
     get the "show the binding only if there's room" behaviour without
     measuring widths in JS. */
  .field-overlay .field-label {
    display: flex; align-items: baseline; gap: 6px;
    flex: 1; min-width: 0; overflow: hidden;
    font-size: inherit; line-height: 1.2;
  }
  /* The 'T' / '#' / '$' type prefix. Serif so 'T' visually anchors to
     a Times-style letter rather than reading as a random capital. */
  .field-overlay .field-label-icon {
    flex: 0 0 auto;
    font-family: 'Times New Roman', Times, Cambria, Georgia, serif;
    font-weight: 700;
    opacity: 0.7;
  }
  /* The field name shrinks with ellipsis after the icon but before
     the hint so the name stays as visible as possible. */
  .field-overlay .field-label-name {
    flex: 0 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* The secondary hint (jsonPath / default). flex:1 1 0 lets it take
     leftover space when the box is wide and collapse to 0 when it
     isn't, so the user sees it only when there's room. 40% alpha
     matches the user spec. */
  .field-overlay .field-label-hint {
    flex: 1 1 0; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    opacity: 0.4; font-style: italic;
  }
  .field-overlay.checkbox {
    min-width: 0; min-height: 0; width: 18px; height: 18px;
    padding: 0; justify-content: center;
    font-weight: bold; font-size: 14px;
  }
  /* Floating radio: small green dot. Form-detected radio overrides
     this with its own purple-themed shape below. */
  .field-overlay.radio {
    min-width: 0; min-height: 0; width: 16px; height: 16px;
    padding: 0; justify-content: center; border-radius: 50%;
    background: rgba(46, 213, 115, 0.2);
  }
  .field-overlay.radio::before {
    content: ''; width: 6px; height: 6px; border-radius: 50%;
    background: var(--success);
  }
  /* Form-detected checkbox/radio overlays sit on the PDF widget rect,
     show a label inside, and inherit the purple palette (defined by
     `.field-overlay.form-detected` above). The dot pseudo-element is
     suppressed because the inner label replaces it. */
  .field-overlay.radio.form-detected { border-radius: 50%; }
  .field-overlay.radio.form-detected::before { display: none; }
  .field-overlay.checkbox.form-detected,
  .field-overlay.radio.form-detected {
    padding: 0 4px;
    justify-content: center;
    font-weight: normal;
    font-size: 10px;
    /* Opaque background so the source PDF's own widget appearance
       (Acrobat draws checkbox boxes with prominent borders, internal
       strokes, and pre-checked glyphs) doesn't bleed through and
       compete with our overlay. The .field-overlay.form-detected base
       rule above uses an 8% accent tint over the PDF — fine for text
       widgets, but it shows the underlying checkbox box with a
       vertical-line artifact and the original /AP check mark when
       the operator zooms or hovers, which reads as a rendering bug.
       Layering an opaque var(--c-bg-card) underneath + the same
       accent tint on top via background-image keeps the previous
       look while fully hiding the widget. */
    background-color: var(--c-bg-card);
    background-image: linear-gradient(rgba(108, 92, 231, 0.10), rgba(108, 92, 231, 0.10));
  }
  .field-overlay.checkbox.form-detected.is-prechecked,
  .field-overlay.radio.form-detected.is-prechecked {
    background-color: var(--c-bg-card);
    background-image: linear-gradient(rgba(108, 92, 231, 0.22), rgba(108, 92, 231, 0.22));
  }
  .field-overlay.checkbox.form-detected.selected,
  .field-overlay.radio.form-detected.selected {
    background-color: var(--c-bg-card);
    background-image: linear-gradient(rgba(108, 92, 231, 0.32), rgba(108, 92, 231, 0.32));
  }
  .field-overlay.image,
  .field-overlay.signature {
    padding: 0;
    /* No `overflow: hidden` here — that was clipping the delete × and
       resize handle (both positioned outside the box at -8px / -4px).
       The img inside is sized 100%/100% so it doesn't bleed out. */
  }
  /* The default fit is overridden inline by FieldOverlay using
     field.objectFit (stretch/contain/cover) — this CSS rule provides
     the size baseline only. Applies to image AND signature overlays
     so the natural pixel size of the source asset doesn't blow past
     the overlay box (signatures are typically 600×200px raw and
     would otherwise overflow a 20×8% field rect entirely). */
  .field-overlay.image .field-img-preview,
  .field-overlay.signature .field-img-preview {
    width: 100%; height: 100%; pointer-events: none;
    /* max-* belt-and-braces against any wrapping flex container that
       would let an intrinsic-sized child push the parent. */
    max-width: 100%; max-height: 100%;
    display: block;
  }
  .field-overlay.image .field-img-empty {
    flex: 1; display: flex; align-items: center; justify-content: center;
    color: var(--text-muted); font-size: 10px;
  }
  .field-resize {
    position: absolute; bottom: -4px; right: -4px;
    width: 12px; height: 12px;
    background: var(--success); border: 2px solid white;
    border-radius: 2px; cursor: nwse-resize;
  }
  .field-delete {
    position: absolute; top: -8px; right: -8px;
    width: 16px; height: 16px; border-radius: 50%;
    background: var(--danger); color: white;
    display: flex; align-items: center; justify-content: center;
    font-size: 11px; line-height: 1; cursor: pointer; border: none;
  }
  /* Hide the X on overlays too short to host it without overlapping
     the resize handle / content. JS adds .is-tiny when the rendered
     pixel height drops below 16px (see FieldOverlay ResizeObserver). */
  .field-overlay.is-tiny .field-delete { display: none; }
  .editor-side {
    width: 320px; background: var(--bg-secondary);
    display: flex; flex-direction: column;
    flex-shrink: 0;
  }
  /* Editor right-rail tab strip. Horizontally scrollable when the
     panel is narrower than the four labels combined (typically below
     ~360px or in compressed locales) — same hidden-scrollbar trick the
     original PDF Microservice Builder uses. */
  .editor-tabs {
    display: flex; border-bottom: 1px solid var(--border);
    overflow-x: auto;
    -ms-overflow-style: none;
    scrollbar-width: none;
  }
  .editor-tabs::-webkit-scrollbar { display: none; }
  .editor-tab {
    flex: 1 0 auto;
    padding: 12px; text-align: center; font-size: 12px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary);
    cursor: pointer; border-bottom: 2px solid transparent;
    white-space: nowrap;
  }
  .editor-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
  .editor-tab-body { flex: 1; overflow: auto; padding: 10px 12px; }
  /* Field list (right rail) — bordered cards stacked vertically.
     Configured fields get a green left rail; unconfigured fade out.
     Padding kept tight so a few dozen fields don't push the user
     into excessive scrolling on smaller screens. */
  .field-list-item {
    padding: 7px 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 4px;
    cursor: pointer;
    transition: all 0.15s;
    background: var(--bg-tertiary);
  }
  .field-list-item:hover { border-color: var(--border-hover); }
  .field-list-item.unconfigured { opacity: 0.65; }
  .field-list-item.unconfigured:hover { opacity: 0.95; }
  .field-list-item.configured { border-left: 2.5px solid rgba(46, 213, 115, 0.7); }
  .field-list-item.active {
    opacity: 1;
    border-color: var(--accent);
    background: rgba(108, 92, 231, 0.06);
  }
  .field-list-item.active.configured { border-left-width: 1px; }
  .field-list-item-header {
    display: flex; align-items: center; justify-content: space-between; gap: 6px;
  }
  .field-list-item-name {
    font-size: 12px; font-weight: 600;
    display: inline-flex; align-items: center; gap: 4px;
    overflow: hidden; text-overflow: ellipsis;
    flex: 1; min-width: 0;
  }
  .field-list-item-path {
    font-size: 11px; font-family: var(--font-mono);
    color: var(--text-muted); margin-top: 1px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .field-list-badge {
    font-size: 9px; font-weight: 700; color: white;
    padding: 2px 6px; border-radius: 3px;
    text-transform: uppercase; letter-spacing: 0.04em;
    flex-shrink: 0;
  }
  .field-list-item .configured-dot {
    width: 6px; height: 6px; border-radius: 50%;
    display: inline-block; flex-shrink: 0; margin-left: 6px;
  }
  .field-list-item .configured-dot.yes {
    background: #2ed573;
    box-shadow: 0 0 4px rgba(46, 213, 115, 0.5);
  }
  .field-list-item .configured-dot.no { background: var(--text-muted); opacity: 0.35; }
  .field-list-section-label {
    margin: 6px 0 4px;
    font-size: 10px; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.06em;
    color: var(--text-muted);
  }
  .field-list-section-label:first-child { margin-top: 0; }
  .field-list-separator {
    height: 1px; background: var(--border); margin: 10px 0 6px;
  }

  .field-row {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 12px; border-radius: var(--radius); cursor: pointer;
    font-size: 13px;
  }
  .field-row:hover { background: var(--bg-tertiary); }
  .field-row.active { background: var(--accent-glow); color: var(--accent); }
  .field-row .type-tag { font-size: 10px; padding: 2px 6px; border-radius: 999px; background: var(--bg-elevated); color: var(--text-muted); text-transform: uppercase; }
  .prop-group { margin-bottom: 10px; }
  .prop-group .label { margin-bottom: 4px; }

  /* Form-field banner shown at the top of PropertiesPanel for fields
     that came from the PDF's AcroForm (vs floating fields the user
     placed manually). The accent colour matches the field's type so
     a glance at the PropertiesPanel makes the kind obvious. */
  .form-field-banner {
    margin-bottom: 12px; padding: 10px 12px;
    border-radius: var(--radius); border: 1px solid var(--accent);
    background: var(--accent-glow);
  }
  .form-field-banner-row {
    display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
  }
  .form-field-banner-name {
    font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .form-field-banner-meta {
    font-size: 11px; color: var(--text-muted);
  }
  .pill-form { font-size: 10px; padding: 2px 8px; }
  .pill-form-radio    { background: rgba(225,112,85,0.15);  color: #e17055; }
  .pill-form-checkbox { background: rgba(0,184,148,0.15);   color: #00b894; }
  .pill-form-text     { background: var(--accent-glow);     color: var(--accent); }

  /* Radio options panel (read-only list of all the radio group's
     widgets). Shown when the selected field has a radioOptions array. */
  .radio-options-list {
    background: var(--bg-tertiary); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 6px 4px;
  }
  .radio-option-row {
    display: flex; align-items: center; gap: 10px;
    padding: 5px 8px; border-radius: 4px; font-size: 12px;
  }
  .radio-option-row.active {
    background: rgba(225,112,85,0.12); color: #e17055; font-weight: 600;
  }
  .radio-option-index {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 22px; height: 18px; padding: 0 6px;
    background: var(--bg-elevated); border-radius: 4px;
    font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary);
  }
  .radio-option-row.active .radio-option-index {
    background: rgba(225,112,85,0.2); color: #e17055;
  }
  .radio-option-label { flex: 1; }
  .radio-option-page { font-size: 11px; color: var(--text-muted); font-family: var(--font-mono); }

  /* JSON-path autocomplete dropdown. Anchored to the input via a
     wrapper so the popover sits flush below it, scrollable when the
     schema has lots of paths. */
  .json-path-autocomplete { position: relative; }
  .json-path-suggestions {
    position: absolute; top: 100%; left: 0; right: 0; z-index: 20;
    margin-top: 2px; max-height: 220px; overflow-y: auto;
    background: var(--bg-secondary); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  }
  .json-path-suggestion-item {
    padding: 7px 10px; font-size: 12px; cursor: pointer;
    font-family: ui-monospace, SF Mono, monospace; color: var(--text-secondary);
  }
  .json-path-suggestion-item:hover,
  .json-path-suggestion-item.highlighted {
    background: var(--accent-glow); color: var(--text-primary);
  }
  .json-path-suggestion-item strong { color: var(--accent); font-weight: 600; }

  /* Three-segment theme picker for Settings → General. Shape mirrors a
     button group; the active segment fills with the accent color. */
  .theme-picker {
    display: inline-flex; gap: 0; border: 1px solid var(--border);
    border-radius: var(--radius); overflow: hidden; background: var(--bg-elevated);
  }
  .theme-option {
    border: none; background: transparent; color: var(--text-secondary);
    padding: 8px 16px; font: 600 12px var(--font-sans); cursor: pointer;
    transition: all 0.15s;
  }
  .theme-option:hover { background: var(--bg-tertiary); color: var(--text-primary); }
  .theme-option.active { background: var(--accent); color: white; }
  .theme-option + .theme-option { border-left: 1px solid var(--border); }
  .prop-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
  .checkbox-row { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; user-select: none; }
  /* Vertical breathing room between stacked checkboxes in a prop-group
     (e.g. Bold + Italic + Required). 8 px is enough to feel like distinct
     rows without breaking the visual grouping. */
  .checkbox-row + .checkbox-row { margin-top: 8px; }
  /* Disabled state — applied by .is-disabled on the label when the
     variant isn't available for the current font. Matches the rest of
     the SPA's disabled treatment (50% opacity, not-allowed cursor) and
     dims both the box and the label text via inherited opacity. */
  .checkbox-row.is-disabled { opacity: 0.5; cursor: not-allowed; }
  .checkbox-row.is-disabled input { cursor: not-allowed; }
  .editor-empty {
    flex: 1; display: flex; align-items: center; justify-content: center;
    color: var(--text-muted); font-size: 13px;
  }
  .upload-dropzone {
    border: 2px dashed var(--border); border-radius: var(--radius-lg);
    padding: 32px 24px; text-align: center; cursor: pointer;
    background: var(--bg-primary); transition: all 0.15s;
  }
  .upload-dropzone:hover, .upload-dropzone.dragover {
    border-color: var(--accent); background: var(--accent-glow);
  }
  .upload-dropzone input[type="file"] { display: none; }
  .breadcrumb { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-secondary); margin-bottom: 16px; }
  .breadcrumb a { cursor: pointer; }

  /* ---------- image picker ---------- */
  /* Picker grid: fixed-size tiles so a library with 1 image doesn't
     stretch into a giant square. The settings page uses image-grid-large
     for its richer card layout below. */
  .image-picker-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, 116px);
    gap: 12px; padding: 4px; justify-content: start;
  }
  .image-picker-modal.dragover {
    border-color: var(--accent);
    box-shadow: 0 0 0 4px var(--accent-glow);
  }
  /* Legacy class kept for backwards-compatibility — used by the
     settings-page image library. Same behaviour as before. */
  .image-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
    gap: 10px; max-height: 360px; overflow-y: auto; padding: 4px;
  }
  .image-tile {
    position: relative; aspect-ratio: 1; border: 2px solid var(--border);
    border-radius: var(--radius); overflow: hidden; cursor: pointer;
    background: var(--bg-primary);
  }
  .image-tile:hover { border-color: var(--accent); }
  .image-tile.selected { border-color: var(--success); }
  .image-tile img { width: 100%; height: 100%; object-fit: contain; }
  .image-tile .name {
    position: absolute; bottom: 0; left: 0; right: 0;
    padding: 4px 6px; font-size: 10px;
    background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
    color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .image-tile .del {
    position: absolute; top: 4px; right: 4px;
    width: 18px; height: 18px; border-radius: 50%;
    background: rgba(0,0,0,0.6); color: white; border: none;
    display: flex; align-items: center; justify-content: center;
    font-size: 12px; cursor: pointer; opacity: 0;
    transition: opacity 0.15s;
  }
  .image-tile:hover .del { opacity: 1; }

  /* Settings → Image Library uses a larger card layout. The picker grid
     (above) is for cramped modal contexts; the library page has room to
     show name + size + actions on each card. */
  .image-grid-large {
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 16px; max-height: none; overflow: visible;
  }
  .image-tile-card {
    aspect-ratio: auto; padding: 0;
    display: flex; flex-direction: column; cursor: default;
  }
  .image-tile-card img {
    aspect-ratio: 4 / 3; background: var(--bg-elevated);
  }
  .image-tile-meta { padding: 10px 12px 8px; }
  .image-tile-meta .name {
    position: static; color: inherit; background: none; padding: 0;
    font-weight: 500; font-size: 13px; cursor: text;
  }
  .image-tile-meta .sub {
    font-size: 11px; color: var(--text-muted); margin-top: 4px;
  }
  .image-tile-actions {
    display: flex; gap: 6px; padding: 0 12px 12px;
    border-top: 1px solid var(--border); padding-top: 10px;
  }
  .image-tile-actions .btn { flex: 1; }

  /* ---------- jobs dashboard ---------- */
  .progress-bar {
    width: 120px; height: 6px; border-radius: 3px;
    background: var(--bg-elevated); overflow: hidden;
  }
  .progress-bar-fill {
    height: 100%; background: var(--accent);
    transition: width 0.3s ease;
  }
  .status-dot {
    display: inline-block; width: 8px; height: 8px; border-radius: 50%;
    margin-right: 6px; vertical-align: middle;
  }
  .status-pending  { background: var(--text-muted); }
  .status-running  { background: var(--warning); animation: pulse 1.5s infinite; }
  .status-completed{ background: var(--success); }
  .status-failed   { background: var(--danger); }
  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
  }

  /* ---------- billing / admin shared ---------- */
  .grid-2 { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
  .grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
  .grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
  @media (max-width: 900px) {
    .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
  }

  .stat-tile { background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 18px; }
  .stat-tile .label { margin-bottom: 8px; }
  .stat-tile .value { font-size: 28px; font-weight: 700; line-height: 1.1; }
  .stat-tile .sub { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }

  .usage-bar { width: 100%; height: 8px; border-radius: 4px; background: var(--bg-elevated); overflow: hidden; margin-top: 12px; }
  .usage-bar-fill { height: 100%; background: var(--accent); transition: width 0.4s ease; }
  .usage-bar-fill.warn { background: var(--warning); }
  .usage-bar-fill.danger { background: var(--danger); }

  .pill {
    display: inline-block; padding: 3px 10px; border-radius: 999px;
    font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
  }
  .pill-active { background: rgba(46, 213, 115, 0.15); color: var(--success); }
  .pill-paid { background: rgba(46, 213, 115, 0.15); color: var(--success); }
  .pill-open { background: rgba(255, 165, 2, 0.15); color: var(--warning); }
  .pill-past_due { background: rgba(255, 71, 87, 0.15); color: var(--danger); }
  .pill-canceled, .pill-void { background: var(--bg-elevated); color: var(--text-muted); }
  .pill-trialing, .pill-incomplete { background: rgba(108, 92, 231, 0.15); color: var(--accent); }
  .pill-draft, .pill-uncollectible { background: var(--bg-elevated); color: var(--text-secondary); }

  .plan-card {
    background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-lg);
    padding: 24px; display: flex; flex-direction: column;
  }
  .plan-card.current { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
  .plan-card .plan-name { font-size: 14px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.06em; }
  .plan-card .plan-price { font-size: 32px; font-weight: 700; margin: 8px 0 4px; }
  .plan-card .plan-price-sub { font-size: 12px; color: var(--text-muted); }
  .plan-card ul { list-style: none; margin: 16px 0; flex: 1; }
  .plan-card li { padding: 6px 0; font-size: 13px; color: var(--text-secondary); }
  .plan-card li::before { content: "✓ "; color: var(--success); font-weight: 700; }

  /* Search bar shared with the Help Center page. Despite the name, it's
     a generic list-page search row (`<input>` capped at 320px) — the
     admin console lives in /admin.html now. */
  .admin-search {
    display: flex; gap: 8px; align-items: center; margin-bottom: 16px;
  }
  .admin-search .input { max-width: 320px; }
  /* Monthly / annual billing-interval segmented toggle. */
  .interval-toggle {
    display: inline-flex; border: 1px solid var(--border);
    border-radius: 8px; overflow: hidden;
  }
  .interval-toggle button {
    padding: 6px 12px; font-size: 12px; font-weight: 600;
    border: none; background: transparent; color: var(--text-muted); cursor: pointer;
  }
  .interval-toggle button.active { background: var(--accent); color: #fff; }

  .drawer-overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000;
    display: flex; justify-content: flex-end;
  }
  .drawer {
    width: 540px; max-width: 100%; height: 100%; background: var(--bg-secondary);
    border-left: 1px solid var(--border); padding: 24px; overflow-y: auto;
  }
  .drawer h2 { font-size: 18px; margin-bottom: 4px; }
  .drawer .sub { font-size: 12px; color: var(--text-secondary); margin-bottom: 16px; }

  /* ---------- faq + ticket ---------- */
  .faq-list .faq-row {
    background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius);
    padding: 14px 16px; margin-bottom: 8px;
  }
  .faq-list .faq-row .q { font-weight: 600; cursor: pointer; }
  .faq-list .faq-row .a {
    margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border);
    color: var(--text-secondary); font-size: 14px; line-height: 1.5; white-space: pre-wrap;
  }

  .ticket-thread { display: flex; flex-direction: column; gap: 10px; }
  .ticket-msg {
    border-radius: var(--radius); padding: 12px 14px; font-size: 13px; line-height: 1.5;
    white-space: pre-wrap;
  }
  .ticket-msg.user { background: var(--bg-tertiary); border: 1px solid var(--border); }
  .ticket-msg.staff { background: var(--accent-glow); border: 1px solid var(--accent); }
  .ticket-msg .meta { font-size: 11px; color: var(--text-muted); margin-bottom: 6px; }

  /* Mobile ticket cards — the desktop table is unreadable when its
     5 columns collapse to a 320-px viewport (the original report:
     "Help page tickets table is unusable on mobile"). The card
     layout below puts subject + status on the top line, category
     and timestamp on a meta line, and a full-width Open button at
     the bottom for an unambiguous tap target. */
  .ticket-card-list { display: flex; flex-direction: column; gap: 8px; padding: 8px; }
  .ticket-card {
    background: var(--bg-tertiary); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 12px;
    display: flex; flex-direction: column; gap: 10px;
  }
  .ticket-card-head {
    display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
  }
  .ticket-card-subject {
    font-weight: 600; font-size: 14px; color: var(--text-primary);
    line-height: 1.35; word-break: break-word;
  }
  .ticket-card-meta {
    display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
    font-size: 12px; color: var(--text-muted);
  }
  .ticket-card .btn { width: 100%; justify-content: center; }

  /* Mobile API key cards — same problem as tickets (7-column table
     clips below 320px). Same shape: name + prefix on the head row,
     workspace badges, wrap-friendly meta row, full-width revoke
     button at the bottom. */
  .api-key-card-list { display: flex; flex-direction: column; gap: 8px; padding: 8px; }
  .api-key-card {
    background: var(--bg-tertiary); border: 1px solid var(--border);
    border-radius: var(--radius); padding: 12px;
    display: flex; flex-direction: column; gap: 10px;
  }
  .api-key-card-head {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    flex-wrap: wrap;
  }
  .api-key-card-name {
    font-weight: 600; font-size: 14px; color: var(--text-primary);
    word-break: break-word;
  }
  .api-key-card-prefix {
    font-size: 11px; color: var(--text-muted);
    font-family: ui-monospace, SF Mono, monospace;
  }
  .api-key-card-grants {
    display: flex; flex-wrap: wrap; gap: 4px;
  }
  .api-key-card-meta {
    display: flex; flex-direction: column; gap: 4px;
    font-size: 12px; color: var(--text-muted);
  }
  .api-key-card .btn { width: 100%; justify-content: center; }

  /* ----- visual polish v3 (2026-05-08) ----- */

  /* Buttons: primary picks up the same gradient + soft shadow as the
     marketing site so the brand feels continuous between pages. */
  .btn-primary {
    background: linear-gradient(135deg, var(--accent) 0%, #8576ee 100%) !important;
    border-color: var(--accent) !important;
    color: #fff !important;
    box-shadow: 0 4px 14px rgba(108, 92, 231, 0.25);
    transition: box-shadow 0.18s, transform 0.18s;
  }
  .btn-primary:hover { box-shadow: 0 6px 18px rgba(108, 92, 231, 0.4); transform: translateY(-1px); }
  .btn-primary:active { transform: translateY(0); }
  .btn-primary:disabled {
    background: var(--bg-elevated) !important;
    border-color: var(--border) !important;
    color: var(--text-muted) !important;
    box-shadow: none; transform: none; cursor: not-allowed;
  }
  .btn { transition: border-color 0.15s, background 0.15s; }
  .btn:hover { border-color: var(--accent); }

  /* Tables: row hover, slightly tighter cells, sticky header for long
     lists. Applies wherever .table is used. */
  .table tbody tr { transition: background 0.12s; }
  .table tbody tr:hover { background: rgba(108, 92, 231, 0.04); }
  .table thead th {
    position: sticky; top: 0; z-index: 1;
    background: var(--bg-secondary);
  }

  /* Empty state: warmer, more inviting tone — borderless on a tinted
     surface instead of the dashed-border look. */
  .empty {
    padding: 60px 24px; text-align: center; color: var(--text-secondary);
    background: linear-gradient(180deg, var(--bg-secondary) 0%, transparent 100%);
    border: 1px solid var(--border); border-radius: var(--radius-lg);
    font-size: 14px;
  }

  /* Cards: subtle hover lift wherever a .card is used as a tile. The
     existing .card baseline stays, just adds polish. */
  .stat-tile, .card {
    transition: border-color 0.15s, box-shadow 0.15s;
  }
  .stat-tile:hover { border-color: rgba(108, 92, 231, 0.4); }

  /* Status pills: tighten typography + add a soft outline so they read
     consistently across the app (billing, tickets, jobs, workspaces). */
  .pill {
    line-height: 1.4;
    border: 1px solid currentColor;
    border-color: transparent;
  }

  /* Top-bar pill: subtle hover to hint it's clickable. */
  .tenant-pill {
    transition: border-color 0.15s, background 0.15s;
  }
  .tenant-pill:hover { background: var(--bg-elevated); }

  /* Avatar dropdown: reduce visual noise on hover. */
  .avatar-dropdown-item {
    transition: background 0.12s, color 0.12s;
  }

  /* Inputs: tighter focus ring matching the brand. */
  .input:focus, .select:focus, textarea.input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.18);
  }
  /* Drag-active visual on a textarea that accepts file drops (e.g. the
     bulk-jobs items input). Same accent ring as focus, plus a soft
     background fill so the drop zone is unmistakable. */
  .input.is-dragover {
    border-color: var(--accent);
    background: var(--accent-glow);
    box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.18);
  }

  /* Section headers in settings — give them a bit more breathing room. */
  .settings-main .page-header { margin-bottom: 28px; }
  .settings-main .page-title { font-size: 24px; }

  /* Settings list-item card. Each settings page (registries, schemas,
     webhooks, workspaces, members) renders its rows as a stack of these
     cards: title + meta on the left, action menu on the right. Same
     layout the original PDF Microservice Builder uses, so subpages
     across the app stay visually consistent. */
  .settings-card {
    display: flex; align-items: center; justify-content: space-between;
    padding: 12px 16px; margin-bottom: 8px;
    background: var(--bg-tertiary); border: 1px solid var(--border);
    border-radius: var(--radius);
  }
  .settings-card-info { flex: 1; min-width: 0; }
  .settings-card-title-row {
    display: flex; align-items: center; gap: 8px;
  }
  .settings-card-title { font-size: 13px; font-weight: 600; color: var(--text-primary); }
  .settings-card-meta {
    font-size: 11px; color: var(--text-muted); font-family: var(--font-mono);
    margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .settings-card-actions {
    display: flex; gap: 6px; flex-shrink: 0; margin-left: 12px; align-items: center;
  }
  .settings-card .pill {
    font-size: 10px; padding: 2px 8px;
  }
  /* Block variant — for in-card forms (label / input stacks). The base
     .settings-card uses a row flex with space-between to lay out a
     trailing "actions" cluster; that squashes any tall content into a
     single line. .settings-card.is-block opts out so labels, inputs,
     radio cards, etc. stack vertically as the caller wrote them. */
  .settings-card.is-block {
    display: block;
  }

  /* Empty state shared by every settings page. Centered, padded so
     it doesn't sit awkwardly against the page-header bottom edge. */
  .settings-empty {
    text-align: center; padding: 48px 16px; color: var(--text-muted);
  }
  .settings-empty p { margin-bottom: 12px; font-size: 13px; }

  /* 3-dot action menu used inside settings-card. The dropdown is
     fixed-positioned by JS so it escapes overflow:hidden ancestors. */
  .action-menu { position: relative; }
  .action-menu-trigger {
    width: 32px; height: 32px; border-radius: var(--radius);
    border: 1px solid transparent; background: transparent;
    color: var(--text-muted); cursor: pointer; display: inline-flex;
    align-items: center; justify-content: center; font-size: 18px;
    line-height: 1; padding: 0;
  }
  .action-menu-trigger:hover {
    background: var(--bg-secondary); border-color: var(--border);
    color: var(--text-primary);
  }
  .action-menu-backdrop { position: fixed; inset: 0; z-index: 199; }
  .action-menu-dropdown {
    position: absolute; right: 0; top: calc(100% + 4px); z-index: 200;
    min-width: 180px; padding: 4px;
    background: var(--bg-secondary); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  }
  .action-menu-item {
    display: flex; align-items: center; gap: 8px; width: 100%;
    padding: 8px 10px; border: none; background: transparent;
    color: var(--text-primary); font: 500 13px var(--font-sans);
    cursor: pointer; border-radius: 4px; text-align: left;
  }
  .action-menu-item:hover { background: var(--bg-tertiary); }
  .action-menu-item.danger { color: var(--danger); }
  .action-menu-item.danger:hover { background: rgba(255, 71, 87, 0.1); }

  /* Image Library settings card variant: thumbnail on the left, the
     usual title/meta/actions to the right. Same outer settings-card
     dimensions so a screen with mixed content lines up neatly. */
  .image-card .image-card-thumb {
    width: 64px; height: 64px; flex-shrink: 0;
    margin-right: 14px; border-radius: 6px; overflow: hidden;
    background: var(--bg-elevated); display: flex; align-items: center; justify-content: center;
  }
  .image-card .image-card-thumb img {
    width: 100%; height: 100%; object-fit: contain;
  }

  /* Mobile responsive layout. The 3-pane editor collapses into a
     single visible pane at <=768px; a bottom nav switches between
     templates / editor / properties. Pretty much exactly what the
     original does. */
  .is-hidden { display: none !important; }
  .templates-3pane-browser-wrap { display: contents; }

  /* Pull-to-refresh indicator. Sits sticky just above the list it
     decorates. translate3d driven directly from usePullToRefresh; the
     arrow/spinner swap is via data-refreshing on the wrapper. */
  .ptr-indicator {
    height: 0;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-muted); opacity: 0;
    transform: translateY(0);
    pointer-events: none;
  }
  .ptr-indicator .ptr-arrow {
    font-size: 18px; line-height: 1;
    transform: rotate(0); transition: transform 0.2s ease;
  }
  .ptr-indicator[data-armed="1"] .ptr-arrow { transform: rotate(180deg); color: var(--accent); }
  .ptr-indicator[data-refreshing="1"] .ptr-arrow { display: none; }
  .ptr-indicator .ptr-spinner {
    display: none;
    width: 18px; height: 18px;
    border: 2px solid var(--border); border-top-color: var(--accent);
    border-radius: 50%; animation: ptr-spin 0.7s linear infinite;
  }
  .ptr-indicator[data-refreshing="1"] .ptr-spinner { display: block; }
  @keyframes ptr-spin { to { transform: rotate(360deg); } }

  /* Indeterminate progress bar. Used everywhere we'd otherwise show
     "Loading…" text or a rotating spinner. Fluid sliding gradient,
     auto-themed via CSS vars (works in both dark and light), and
     respects prefers-reduced-motion (falls back to a static accent
     fill so the user still gets a clear "something is happening"
     affordance without the moving gradient). */
  .progress-bar {
    position: relative;
    width: 100%;
    height: 3px;
    background: var(--bg-tertiary);
    overflow: hidden;
    border-radius: 999px;
  }
  .progress-bar::after {
    content: '';
    position: absolute;
    top: 0; bottom: 0;
    left: 0; right: 0;
    background: linear-gradient(90deg,
      transparent 0%,
      var(--accent) 50%,
      transparent 100%);
    animation: progress-slide 1.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
    transform: translateX(-100%);
    will-change: transform;
  }
  @keyframes progress-slide {
    0%   { transform: translateX(-100%); }
    100% { transform: translateX(100%); }
  }
  @media (prefers-reduced-motion: reduce) {
    .progress-bar::after {
      animation: none;
      transform: translateX(0);
      background: var(--accent);
      opacity: 0.4;
    }
  }

  /* Skeleton frame: a centred loading affordance that fills its parent.
     Use for the PDF editor canvas placeholder and any other "the whole
     content area is waiting" state. The progress-bar lives inside so
     the user gets motion + a labeled hint, never just blank space. */
  .loading-frame {
    width: 100%;
    min-height: 320px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 28px 20px;
    border: 1px dashed var(--border);
    border-radius: 10px;
    background: var(--bg-secondary);
    color: var(--text-secondary);
  }
  .loading-frame__label {
    font-size: 13px;
    font-weight: 500;
    letter-spacing: 0.01em;
    color: var(--text-secondary);
  }
  .loading-frame .progress-bar {
    max-width: 240px;
  }
  /* Smaller variant for inline / card slots. */
  .loading-inline {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 0;
    color: var(--text-secondary);
    font-size: 13px;
  }
  .loading-inline .progress-bar {
    flex: 1;
    max-width: 140px;
  }
  /* Mobile: keep the frame compact so it doesn't blow up the viewport
     on small phones in landscape. */
  @media (max-width: 600px) {
    .loading-frame { min-height: 200px; padding: 20px 14px; }
  }

  .mobile-bottom-nav {
    position: fixed; bottom: 0; left: 0; right: 0; z-index: 50;
    display: flex; align-items: stretch;
    background: var(--bg-secondary); border-top: 1px solid var(--border);
    padding: 6px; gap: 6px;
    /* Account for iOS home-indicator safe area. */
    padding-bottom: max(6px, env(safe-area-inset-bottom));
  }
  /* Bottom-nav is position: fixed so it always overlays content. Every
     scrollable surface on mobile needs to reserve this much room at the
     bottom or its last items sit under the nav and become unreachable
     (the original report: "delete template" button hidden in the
     properties → settings tab). Approx. nav height: 6 + ~38 (icon + 4 +
     label) + 6 = 50px, then iOS safe-area on top. Round to 72 for
     comfortable thumb clearance. Used by all the mobile-only paddings
     in the 768px / 600px media queries below. */
  :root { --mobile-bottom-nav-clearance: calc(72px + env(safe-area-inset-bottom)); }
  .mobile-nav-btn {
    flex: 1;
    display: inline-flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 4px;
    background: transparent; border: none;
    color: var(--text-muted);
    font: 600 11px var(--font-sans);
    padding: 8px 4px; border-radius: var(--radius);
    cursor: pointer;
  }
  .mobile-nav-btn:hover { color: var(--text-secondary); }
  .mobile-nav-btn.active { color: var(--accent); background: var(--accent-glow); }
  .mobile-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; }
  .mobile-nav-btn span { font-size: 10px; }
  /* Force every icon in the bottom nav to a consistent size — Icons.File
     ships at 44×44 because it's reused in the empty-template hero, which
     would otherwise dwarf the 14×14 Folder/Settings icons next to it. */
  .mobile-nav-btn svg { width: 22px; height: 22px; }

  @media (max-width: 1024px) {
    .template-browser { width: 240px; }
    .editor-side { width: 280px; }
    .topbar { padding: 0 16px; }
  }

  @media (max-width: 768px) {
    /* Switch to single-pane: each pane fills the viewport, the
       bottom-nav-button toggles which one is visible. */
    .templates-3pane.is-mobile {
      flex-direction: column;
      /* Leave room for the bottom nav (~58px). */
      padding-bottom: 64px;
    }
    .templates-3pane-browser-wrap.is-hidden { display: none !important; }
    .templates-3pane.is-mobile .template-browser {
      width: 100%; flex: 1; min-height: 0;
      border-right: none; border-bottom: 1px solid var(--border);
    }
    .templates-3pane.is-mobile .templates-3pane-editor.is-hidden { display: none !important; }
    .templates-3pane.is-mobile .templates-3pane-editor {
      width: 100%; flex: 1; min-height: 0;
    }
    .templates-3pane.is-mobile .editor-layout {
      flex-direction: column;
    }
    .editor-canvas-area.is-hidden,
    .editor-side.is-hidden { display: none !important; }
    .editor-side.is-mobile {
      width: 100%; height: 100%; flex: 1; min-height: 0;
      border-left: none;
    }
    /* When .editor-layout flips to flex-direction: column on mobile,
       .editor-canvas-area becomes the vertical-flex child. Its `min-width:
       0` (set on desktop) is for row layouts; in column layouts the
       analogue is `min-height: 0`. Without this, the inner .canvas-scroll
       expands to fit content and its `overflow: auto` never engages on
       the vertical axis — which is the "PDF only scrolls horizontally"
       bug on mobile. */
    .editor-canvas-area { min-height: 0; }

    /* Bottom-nav clearance — see :root --mobile-bottom-nav-clearance.
       Every scrollable surface in the editor needs room at the bottom
       so the last items aren't hidden by the fixed bottom nav. */
    .canvas-scroll,
    .editor-tab-body,
    .template-browser-list {
      padding-bottom: var(--mobile-bottom-nav-clearance);
    }

    .topbar-actions .btn-text-mobile { display: none; }
    /* Generalized: any .btn-text-mobile inside a tight toolbar collapses
       on small viewports so we keep icon-only buttons. */
    .editor-toolbar .btn-text-mobile { display: none; }
    .modal { width: 95vw; max-height: 90vh; padding: 20px; }

    /* Workspace pill needs a tighter cap on phones — the topbar has logo
       on the left and avatar on the right competing for space. */
    .tenant-pill { max-width: min(50vw, 220px); padding: 6px 10px; font-size: 12px; }

    /* Wide tables (admin console, API keys, members, webhooks, render
       history, …) get clipped on mobile because html/body and .layout
       all set `overflow: hidden` — there's no document-level scroll to
       fall back on. Make every direct parent of a .table its own
       horizontal scroll region, and let the table itself reach its
       natural content width instead of cramming into the viewport. The
       :has() selector lets us apply this without retrofitting every
       call site with a wrapper class. */
    *:has(> .table) {
      overflow-x: auto;
      -webkit-overflow-scrolling: touch;
      max-width: 100%;
    }
    .table { min-width: max-content; }
    /* Match-content min-width pulls table cells out to their natural
       size. Cells that were truncating with ellipsis on desktop want
       their full content shown on mobile too — otherwise the user
       scrolls into a column that still says "Long entry name…" with
       no recourse. Bias toward showing the data; visual density on
       phones is the wrong constraint when the table is already a
       scroll region. */
    .table td,
    .table th { white-space: nowrap; }
  }

  @media (max-width: 600px) {
    .topbar { height: 48px; padding: 0 12px; }
    .topbar-logo { font-size: 13px; gap: 8px; }
    .topbar-actions { gap: 4px; }

    .template-browser-header { padding: 10px 12px 6px; }
    .template-browser-list { padding: 0 8px 16px; }
    .template-card { padding: 12px; }

    /* Editor toolbar on mobile: NEVER wrap, NEVER scroll, NEVER clip.
       The combined width of every group has to fit the viewport.
       Pinch handles zoom natively so the dedicated zoom group is
       hidden here, freeing space for Place / Hide / Pagination.
       Drop the "Page " prefix on the page label so it reads "1 / 2"
       instead. */
    .editor-toolbar { padding: 6px 8px; gap: 6px; flex-wrap: nowrap; overflow: hidden; min-width: 0; }
    .editor-toolbar > .spacer { flex: 1 1 0; min-width: 0; }
    .editor-toolbar .page-nav { gap: 4px; font-size: 13px; flex-shrink: 0; min-width: 0; }
    .editor-toolbar .page-nav-pages button { padding: 4px 12px; min-width: 36px; font-size: 13px; }
    .editor-toolbar .zoom-controls { display: none; }
    .editor-toolbar .page-prefix { display: none; }
    .editor-toolbar .page-label { white-space: nowrap; padding: 0 6px; }
    /* Tighter horizontal padding on phones — but keep the bottom-nav
       clearance from the 768px block; a flat `padding: 12px` would put
       the canvas's last row back under the fixed bottom nav. */
    .canvas-scroll { padding: 12px 12px var(--mobile-bottom-nav-clearance); }
    .editor-tab { padding: 10px 8px; font-size: 10px; }

    /* iOS Safari zooms when an input has font-size <16. Bump explicitly. */
    .input, .select, textarea.input { font-size: 16px; }
  }

  /* Touch-friendly: bigger tap targets on coarse pointers. */
  @media (hover: none) and (pointer: coarse) {
    .btn { min-height: 44px; }
    .btn.btn-icon { min-height: 36px; }
    /* Small buttons (used in dense toolbars like the templates pane
       header) get matched dimensions so an icon-only button doesn't
       end up taller than its sibling text button. Both 36px high,
       icon button square. */
    .btn-sm { min-height: 36px; }
    .btn-sm.btn-icon { width: 36px; min-width: 36px; height: 36px; }
    .template-card { padding: 16px 12px; }
  }

  /* Editor canvas-area: give the empty state ("select a template…")
     more vertical centering than the bare text. */
  .editor-empty {
    flex: 1;
    display: flex; align-items: center; justify-content: center;
    color: var(--text-muted); font-size: 14px;
    background: linear-gradient(180deg, transparent 0%, rgba(108, 92, 231, 0.03) 100%);
  }
