:root {
  --bg: #0a0d10;
  --bg-raised: #10151a;
  --border: #1f2830;
  --text: #e6edf1;
  --text-dim: #8b9aa5;
  --accent: #00e5b4;
  --accent-dim: #00b590;
  --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
}

* { box-sizing: border-box; }

html { scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

.wrap {
  max-width: 960px;
  margin: 0 auto;
  padding: 0 24px;
}

a { color: inherit; text-decoration: none; }

code {
  font-family: var(--mono);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.1em 0.4em;
  font-size: 0.9em;
}

/* cs-topbar (widgets/topbar.js) renders its own chrome in a shadow root -
   nothing here targets it. The rules below are this page's own content. */

.lang-switcher {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 4px;
  font-size: 0.82rem;
  padding: 4px 6px;
}

/* buttons */

.btn-primary,
.btn-ghost {
  display: inline-block;
  font-size: 0.9rem;
  font-weight: 600;
  padding: 10px 18px;
  border-radius: 6px;
  white-space: nowrap;
  cursor: pointer;
}

.btn-primary {
  background: var(--accent);
  color: #04120d;
  border: none;
}

.btn-primary:hover { background: var(--accent-dim); }

.btn-ghost {
  /* --border (#1f2830) against --bg (#0a0d10) is barely-there contrast -
     read as plain text with no visible button shape at all next to a
     solid .btn-primary/.btn-danger (2026-08-20 admin panel feedback:
     "Edit plan"/"Create in Stripe" didn't look like buttons). Raised
     fill + --text-dim border gives it a real, if still secondary,
     outline at rest instead of only on :hover. */
  background: var(--bg-raised);
  border: 1px solid var(--text-dim);
  color: var(--text);
  cursor: pointer;
}

.btn-ghost:hover { border-color: var(--accent-dim); }

/* Admin-only third variant (Remove, etc.) - the marketing pages never
   need a destructive action button, only the admin portal does. */
.btn-danger {
  display: inline-block;
  font-size: 0.9rem;
  font-weight: 600;
  padding: 10px 18px;
  border-radius: 6px;
  white-space: nowrap;
  background: #ff6b6b;
  color: #2a0a0a;
  border: none;
  cursor: pointer;
}

.btn-danger:hover { background: #ff8787; }

/* One button footprint across the app: centre the label and hold a
   common minimum width so a row of actions (Cancel / Save / Delete, a
   toolbar, a popup's buttons) lines up on a grid instead of each button
   hugging its own text. `white-space: nowrap` above keeps a longer label
   ("Reset password") on one line as the button expands past the min.
   `.admin-content` below tightens the min for the dense portal tier. */
.btn-primary,
.btn-ghost,
.btn-danger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 120px;
  box-sizing: border-box;
}

/* Icon + text (admin.js/org.js/topbar.js's own icon-button helpers,
   icons.js's icon set) - an additive class, not folded into .btn-
   primary/.btn-ghost/.btn-danger directly, since those three are used
   site-wide (marketing CTAs included) and most of those callers never
   pass an icon at all - .icon-text-btn only changes `display`/`gap` for
   the specific buttons that opt in, everything else about the button
   (color/padding/border) still comes from its base class unchanged. */
.icon-text-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.icon-text-btn svg {
  flex: none;
  display: block;
  width: 15px;
  height: 15px;
}

.cta-row { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 32px; }

/* type */

.eyebrow {
  font-family: var(--mono);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.78rem;
  color: var(--accent);
  margin: 0 0 12px;
}

h1, h2, h3 { font-weight: 700; letter-spacing: -0.02em; margin: 0; }

.lede, .section-lede {
  color: var(--text-dim);
  font-size: 1.05rem;
  max-width: 620px;
  margin-top: 18px;
}

/* hero */

.hero { padding: 110px 0 90px; border-bottom: 1px solid var(--border); }

.hero h1 { font-size: clamp(2rem, 4.5vw, 3.1rem); line-height: 1.12; }

/* sections */

section:not(.hero) { padding: 88px 0; border-bottom: 1px solid var(--border); }

/* demo */

.demo-video {
  display: block;
  width: 100%;
  max-width: 860px;
  margin-top: 28px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--bg-raised);
}

section h2 { font-size: clamp(1.6rem, 3vw, 2.1rem); margin-bottom: 16px; }

/* try-it (landing page's unauthenticated hex-paste demo, index.html's
   #try) */

.tryit-panel {
  margin-top: 28px;
  max-width: 860px;
}

.tryit-textarea {
  display: block;
  width: 100%;
  box-sizing: border-box;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 10px;
  padding: 14px 16px;
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1.5;
  resize: vertical;
}

.tryit-textarea:focus { outline: none; border-color: var(--accent-dim); }

.tryit-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 14px;
  flex-wrap: wrap;
}

.tryit-error {
  font-size: 0.85rem;
  color: #ff6b6b;
  margin: 14px 0 0;
}

.tryit-results {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-top: 24px;
}

.tryit-result-item {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 16px 18px;
}

/* Wireshark-style hex dump (tryit.js's toHexRows/buildHexDump) - same
   formatting and colors as computingsphere_dnp3's own ProtocolPage.vue
   hex panel (.hex-dump/.hex-line/.offset/.hex/.ascii there). */

.tryit-hex-dump {
  font-family: var(--mono);
  font-size: 0.72rem;
  line-height: 1.5;
  white-space: pre;
  overflow-x: auto;
  margin-bottom: 12px;
}

.tryit-hex-line { display: flex; gap: 14px; }

.tryit-hex-offset { color: var(--text-dim); }

.tryit-hex-bytes { color: var(--accent-dim); }

.tryit-hex-ascii { color: var(--text-dim); }

.tryit-result-error {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: #ff6b6b;
  margin: 0;
}

/* Protocol tree (tryit.js's buildTreeNode/buildPointsTable/buildIinTable) -
   same visual language as computingsphere_dnp3's own MessageTreeNode.vue/
   PointsTable.vue/IinFlagsTable.vue (font size, colors, table shape),
   ported to plain CSS since this page has no Vue scoped-style equivalent. */

.tryit-tree { overflow-x: auto; }

.tree-node {
  font-family: var(--mono);
  font-size: 0.75rem;
  padding: 2px 0;
  display: flex;
  gap: 6px;
  align-items: baseline;
}

.tree-node.expandable { cursor: pointer; }

.tree-chevron,
.tree-chevron-spacer {
  color: var(--text-dim);
  width: 12px;
  flex-shrink: 0;
  display: inline-block;
}

.tree-label { color: var(--text-dim); }

.tree-value { color: var(--text); }

.tree-points-table,
.tree-iin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.75rem;
  font-family: var(--mono);
  margin: 4px 0 8px;
}

.tree-points-table th,
.tree-points-table td,
.tree-iin-table th,
.tree-iin-table td {
  text-align: left;
  padding: 3px 8px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.tree-points-table th,
.tree-iin-table th {
  color: var(--text-dim);
  font-weight: 500;
  text-transform: uppercase;
  font-size: 0.65rem;
  letter-spacing: 0.04em;
}

.tree-points-table td { color: var(--text); }

.tree-iin-table td { color: var(--text-dim); }

.tree-iin-table tr.set td { color: var(--text); font-weight: 600; }

/* feature diagram (index.html's #pipeline, now the first section after
   the hero, above #demo) - stays inside the standard 960px .wrap like
   every other section (an earlier version widened this to 1180px for a
   single-row 5-across layout, but that made the page's widest section
   also its most cramped-looking one, wider than the hero/demo/pricing
   columns above and below it). The grid below wraps instead. */
.feature-diagram {
  position: relative;
  overflow: hidden;
}

/* Faint horizontal rule field behind the section - a quiet nod to the
   hero copy's "close to the wire", not decoration competing with the
   diagram itself. */
.wire-field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to bottom,
    color-mix(in srgb, var(--accent) 5%, transparent) 0px,
    color-mix(in srgb, var(--accent) 5%, transparent) 1px,
    transparent 1px,
    transparent 96px
  );
  mask-image: radial-gradient(ellipse 80% 60% at 50% 40%, black, transparent);
}

/* Two columns (stages 1-4 as 2 rows of 2), the fifth/final stage spanning
   both to close the grid instead of leaving a lone card half the row's
   width - same "wrap instead of squeeze" idea .principles below uses,
   just a fixed 2-up rather than auto-fit since 5 stages read better as a
   deliberate 2/2/1 than however many auto-fit happens to pack in. */
.pipeline {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin-top: 56px;
  position: relative;
}

.stage-final {
  grid-column: 1 / -1;
}

.stage {
  min-width: 0;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 22px 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.stage:hover {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
  background: color-mix(in srgb, var(--accent) 4%, var(--bg-raised));
}

.stage-head {
  display: flex;
  align-items: center;
  gap: 10px;
}

.stage-icon {
  width: 30px;
  height: 30px;
  flex-shrink: 0;
  border-radius: 7px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
}

.stage-icon svg { width: 16px; height: 16px; }

.stage-index {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--text-dim);
  letter-spacing: 0.04em;
}

.stage h3 {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.stage p {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.86rem;
  line-height: 1.5;
  flex: 1 0 auto;
}

/* Small, honest recreations of the product's own UI (a capture row, a
   hex dump, function-code pills, named points, a report file) rather
   than generic icon decoration - each one is what that stage actually
   produces. */
.proof {
  font-family: var(--mono);
  font-size: 0.72rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.proof-row {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
}

.proof-row .name { color: var(--text); overflow: hidden; text-overflow: ellipsis; }
.proof-row .fill { flex: 1; }

.proof .pill {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 2px 8px;
  font-size: 0.68rem;
  color: var(--text-dim);
  flex-shrink: 0;
}

.proof .pill.ok { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.proof .pill.warn { color: #e2a03f; border-color: color-mix(in srgb, #e2a03f 45%, var(--border)); }

.hexdump { color: color-mix(in srgb, var(--accent) 75%, var(--text-dim)); letter-spacing: 0.02em; }

.proof .dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.proof .dot.d1 { background: #ff6b6b; }
.proof .dot.d2 { background: #e2a03f; }
.proof .dot.d3 { background: var(--accent); }

.proof .chip {
  display: inline-flex;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
  border-radius: 4px;
  padding: 1px 7px;
  font-size: 0.68rem;
}

.doc-row { display: flex; align-items: center; gap: 8px; }
.doc-icon { color: var(--accent); flex-shrink: 0; }
.doc-icon svg { width: 18px; height: 18px; display: block; }
.doc-meta { display: flex; flex-direction: column; gap: 2px; overflow: hidden; }
.doc-meta .fname { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-meta .fsize { color: var(--text-dim); }

/* What this stage hands to the next one - replaces the old between-card
   arrow connector, which only worked in a single-row layout. Reuses the
   same mono/dim "tag" look that connector's own label had. */
.stage-output {
  font-family: var(--mono);
  font-size: 0.64rem;
  color: var(--text-dim);
  letter-spacing: 0.02em;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.stage-output::before {
  content: "→ ";
  color: var(--accent-dim);
}

@media (max-width: 640px) {
  .pipeline { grid-template-columns: 1fr; }
  .stage-final { grid-column: auto; }
}

.principles {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 20px;
  margin: 40px 0 0;
  padding: 0;
}

.principles li {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 22px;
}

.principles h3 {
  font-size: 1rem;
  color: var(--accent);
  margin-bottom: 8px;
}

.principles p {
  color: var(--text-dim);
  font-size: 0.92rem;
  margin: 0;
}

.status .btn-primary { margin-top: 28px; }

/* pricing (index.html's Pricing section, signup.js's modal) */

.plan-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
  margin: 40px 0 0;
  max-width: 940px;
}

/* Only one plan left (the user's own ask, 2026-09-18: free-plan only) -
   the auto-fit grid above would otherwise stretch a single card across
   the full 940px row. */
.plan-cards-single {
  max-width: 340px;
}

.pricing-note {
  color: var(--text-dim);
  font-size: 0.9rem;
  margin: 16px 0 0;
}

.plan-card {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 24px;
}

.plan-card h3 {
  font-size: 1.05rem;
  margin-bottom: 6px;
}

.plan-card-price {
  font-family: var(--mono);
  font-size: 1.4rem;
  font-weight: 700;
  margin: 0 0 12px;
}

.plan-card-desc {
  color: var(--text-dim);
  font-size: 0.88rem;
  margin: 0 0 20px;
  line-height: 1.5;
}

.plan-card button {
  width: 100%;
}

/* The paid plan, visually distinguished the same way org/org.js's own
   plan-upsell card is (an accent border/tint) - one consistent way this
   site signals "the plan you'd pay for" wherever it's shown. */
.plan-card-featured {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
  background: color-mix(in srgb, var(--accent) 6%, var(--bg-raised));
}

/* footer */

.site-footer {
  padding: 32px 0 60px;
  color: var(--text-dim);
  font-size: 0.85rem;
}

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

.site-footer a:hover { color: var(--text); }

/* account / users (see account.js) - <main class="admin-main"> +
   .admin-shell, same sidebar-and-panel shape org/org.js and admin/admin.js
   already use, not a page-specific layout. .account-header (just the back
   link/unsaved banner - .profile-band moved inside the Profile panel
   itself, alongside the edit form, so this page's own top section is the
   same size as org/admin's plain h1) matches .admin-shell's own
   max-width/centering so both sit at the same width instead of the
   header looking edge-to-edge while the shell below it is centered
   narrower. */
.account-header {
  max-width: 1400px;
  margin: 0 auto;
}

.back-link {
  display: inline-block;
  color: var(--text-dim);
  font-size: 0.85rem;
  margin-bottom: 24px;
}

.back-link:hover { color: var(--text); }

.dim { color: var(--text-dim); }

.mono { font-family: var(--mono); }

/* services/ (Authelia's post-login landing page - see services.js) */

.services-main { padding: 56px 0; }

.services-main h1 {
  font-size: 1.4rem;
  margin-bottom: 8px;
}

.services-subtitle {
  font-size: 0.95rem;
  margin: 0 0 32px;
}

.service-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 20px;
  max-width: 620px;
}

/* Reuses .plan-card's own look (pricing section above) - a service card
   is the same "one option in a short list, pick one" shape a plan card
   is, just linking out instead of triggering signup. */
.service-card {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 24px;
  /* Column so the action (button / "no access" note) can be pinned to
     the card's bottom edge - descriptions vary in length between
     services and the grid already stretches every card to the tallest,
     so without this the buttons sit at different heights. */
  display: flex;
  flex-direction: column;
}

.service-card h3 {
  font-size: 1.05rem;
  margin-bottom: 6px;
}

.service-card-desc {
  color: var(--text-dim);
  font-size: 0.88rem;
  margin: 0 0 20px;
  line-height: 1.5;
  flex: 1; /* eats the slack so the action below lands on the bottom edge */
}

.service-card .btn-primary { width: 100%; text-align: center; }

.service-card-disabled { opacity: 0.55; }

.service-card-locked {
  display: block;
  color: var(--text-dim);
  font-size: 0.85rem;
  text-align: center;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Identity band inside the Profile panel's own content column (account/
   index.html - used to sit full-width above the whole .admin-shell grid,
   moved down here so the page's top section matches org/admin's plain
   h1 size) - avatar, name/email, org + org-role, and the
   (Authelia-groups) roles list. */
/* `section.profile-band` (not just `.profile-band`) - matches
   `section:not(.hero)`'s own specificity (element + one class each) so
   source order decides the tie instead of that landing-page rule always
   winning regardless of order. Without this, every dashboard card built
   from a bare <section> (this one, .edit-card, .credits-card below)
   silently got the marketing page's own 88px-top/bottom-0-sides padding
   instead of its own - the landing page's hero/pricing/features sections
   are full-viewport blocks that actually want that; a compact bordered
   card on /account/ very much doesn't. Confirmed live: that's exactly
   what was making the Save button below look "lost" - not the button,
   the card's own padding around it. */
section.profile-band {
  display: flex;
  gap: 20px;
  align-items: center;
  flex-wrap: wrap;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 24px;
  margin-bottom: 20px;
}

.profile-band-info { min-width: 0; }

.display-name-lg { font-size: 1.3rem; font-weight: 700; margin: 0; }

.email { font-size: 0.85rem; margin-top: 4px; }

.org-line {
  margin-top: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.85rem;
}

.org-role-badge {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 0.75rem;
  font-weight: 600;
}

/* Org brand logo (org.js's renderLogoSection) - same drag-and-drop,
   upload-on-drop-or-pick shape as .avatar-dropzone above, just its own
   class family (org-logo-*) since org.js toggles/queries these by class
   independently of the profile page. Still uploads immediately on
   change (no batching into a form) - a logo has nothing else to batch it
   with. */
.org-logo-section {
  margin-top: 16px;
  display: flex;
  align-items: center;
  gap: 12px;
}

.org-logo-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px;
  border: 1px dashed transparent;
  border-radius: 12px;
  cursor: pointer;
}

.org-logo-dropzone:hover,
.org-logo-dropzone:focus-visible {
  border-color: var(--border);
  outline: none;
}

.org-logo-dropzone.dropzone-active {
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.org-logo-hint {
  font-size: 0.7rem;
  text-align: center;
  max-width: 90px;
}

.org-logo-preview {
  width: 64px;
  height: 64px;
  border-radius: 8px;
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  flex-shrink: 0;
}

.org-logo-preview img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.org-logo-placeholder {
  font-size: 0.7rem;
  text-align: center;
  padding: 4px;
}

.org-logo-actions {
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: flex-start;
}

.org-logo-status {
  font-size: 0.75rem;
}

.roles {
  margin-top: 10px;
  display: flex;
  gap: 8px;
  align-items: flex-start;
  font-size: 0.8rem;
}

.role-label { margin-right: 2px; margin-top: 1px; }

/* Roles/org-role render as a list, not tags/pills - see ../README.md
   "Account / Users". Each item gets the same small accent dot the brand
   mark in widgets/topbar.js uses (.brand .dot there), for visual
   continuity instead of a plain bullet. */
/* Row + wrap, not a stacked column - a 1-3 item role list (the common
   case) reads as a tight inline row here; still a list of dotted items,
   not pills (see account.js's roleList comment), just laid out
   horizontally so it doesn't eat a full line of vertical rhythm per
   role. Wraps to a second line once there are enough roles to need it. */
.role-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  row-gap: 4px;
  column-gap: 16px;
}

.role-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: 0.8rem;
}

.role-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
}

.avatar {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--accent);
  color: #04120d;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 18px;
  flex-shrink: 0;
  overflow: hidden;
}

.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.avatar-lg {
  width: 76px;
  height: 76px;
  font-size: 28px;
}

/* Drag-and-drop avatar upload (account.js's initAvatarDropzone) - wraps
   the avatar preview itself rather than a separate box, so dropping an
   image where the avatar already is feels natural. Click/Enter/Space all
   open the underlying (visually-hidden) file input as a fallback. */
.avatar-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 8px;
  border: 1px dashed transparent;
  border-radius: 12px;
  cursor: pointer;
}

.avatar-dropzone:hover,
.avatar-dropzone:focus-visible {
  border-color: var(--border);
  outline: none;
}

.avatar-dropzone.dropzone-active {
  border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.avatar-dropzone-hint {
  font-size: 0.7rem;
  text-align: center;
  max-width: 90px;
}

/* Edit profile, then Credits, stacked full-width rather than a
   side-by-side grid - the two cards' content is rarely close to the
   same height (Credits is a couple of lines, Edit profile is a whole
   form), so a 2-column grid left one side stretched with dead air next
   to the other. Stacked, each card is exactly as tall as its own
   content. */
.account-grid {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

/* `section.edit-card` - see .profile-band's own comment above for why
   the bare element selector is needed here too. */
section.edit-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
}

.edit-card h2 {
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin-bottom: 14px;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.85rem;
  margin-bottom: 14px;
}

.field-hint {
  margin: -2px 0 0;
  font-size: 0.78rem;
  color: var(--text-dim);
}

.field input[type="text"],
.field input[type="file"] {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.9rem;
  font-family: inherit;
}

.edit-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 4px;
}

/* Account page (account.js's markUnsaved/clearUnsaved) - shown once the
   display name or avatar has changed but hasn't been saved yet, so the
   Save button (buried further down inside .edit-card) doesn't get missed.
   The Save button here is the same submit as the profile form's own
   (form="profile-form" attribute, no duplicate JS wiring needed). */
.unsaved-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: color-mix(in srgb, orange 12%, var(--bg-raised));
  border: 1px solid color-mix(in srgb, orange 40%, var(--border));
  color: var(--text);
  border-radius: 8px;
  padding: 10px 16px;
  margin-bottom: 20px;
  font-size: 0.9rem;
}

/* [hidden] and a class-based `display` declaration have equal CSS
   specificity, so without this the plain .unsaved-banner rule above wins
   the cascade and the element never actually hides - this selector is
   more specific (0,2,0 vs 0,1,0), so toggling the `hidden` attribute in
   account.js's markUnsaved()/clearUnsaved() works as expected. */
.unsaved-banner[hidden] {
  display: none;
}

.unsaved-banner .btn-sm {
  padding: 6px 14px;
  font-size: 0.85rem;
  border-radius: 6px;
}

.edit-actions button {
  border: none;
  cursor: pointer;
}

/* "Change password" and "Manage two-factor authentication" - both link
   out to Authelia's own portal (../computingsphere_auth/authelia's
   /reset-password and /settings/two-factor-authentication respectively),
   not pages this app renders itself, so they sit side by side as a row
   of ghost buttons rather than one being singled out. */
.security-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* Groups "Change password" under its own small label instead of leaving
   it as a lone button floating right under Save - same border-top +
   spacing .billing-section (org.js) already uses to separate a card into
   sub-sections. */
.edit-card-divider {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.edit-card-divider h2 {
  margin-bottom: 10px;
}

/* `section.credits-card` - see .profile-band's own comment above for why
   the bare element selector is needed here too. */
section.credits-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
}

.credits-card h2 {
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin-bottom: 12px;
}

.credits-card .btn-primary {
  display: inline-block;
}

/* `.icon-text-btn`'s own `display: inline-flex` (style.css, near
   .field-hint) loses to the rule right above here on specificity alone
   (two classes beats one) - without this override the icon and label
   stack on separate lines instead of sitting side by side, since
   inline-block gives the <svg> (itself `display: block`) nothing to sit
   next to. */
.credits-card .btn-primary.icon-text-btn {
  display: inline-flex;
}

.credits-card-desc {
  color: var(--text-dim);
  font-size: 0.88rem;
  margin: 0 0 18px;
  line-height: 1.5;
}

/* Reuses .plan-upsell's accent border/tint (pricing section) - both are
   "a highlighted card pointing at one paid-account action", just reached
   from different pages. Kept as its own class, not a plain alias,
   since account.js toggles #credits' `hidden` attribute directly and a
   shared class would make that harder to trace. */
.credits-card-cta {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
  background: color-mix(in srgb, var(--accent) 6%, var(--bg-raised));
}

/* org/org.js's Billing & Credits panel (renderBillingSection) - plan
   status + the single Upgrade/Manage billing action, both Stripe-hosted
   redirects (Checkout, Customer Portal). */
.billing-section {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.billing-section p {
  margin: 0 0 8px;
}

.billing-section button {
  margin-top: 4px;
}

/* The Free-plan branch's upsell card (org/org.js's renderBillingSection)
   - visually separated from the "Free plan"/reset-date status above it,
   so "Unlimited messages, 500MB, up to 2 users" reads as what SphereOps
   includes, not as something the Free plan already has. */
.plan-upsell {
  margin-top: 16px;
  padding: 18px;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border-radius: 10px;
}

.plan-upsell-eyebrow {
  margin: 0 0 6px;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  font-weight: 600;
}

.plan-upsell-title {
  margin: 0 0 8px;
  font-size: 1.05rem;
  font-weight: 700;
}

.plan-upsell-includes {
  margin: 0 0 14px;
  font-size: 0.85rem;
  color: var(--text-dim);
}

.plan-upsell button {
  margin-top: 0;
}

.credits-summary {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  font-size: 0.9rem;
}

.tier-badge {
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  color: var(--accent);
  border-radius: 4px;
  padding: 2px 8px;
  font-size: 0.8rem;
}

.history-list { list-style: none; margin: 0; padding: 0; }

.history-row {
  display: flex;
  gap: 12px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.82rem;
}

.history-row .action { flex: 1; }

.history-row .amount { color: var(--text-dim); font-family: var(--mono); }

.history-row .amount.positive { color: var(--accent); }

/* Audit-log list (admin/admin.js + org/org.js): cap the height and
   scroll within, so a long history scrolls in place instead of pushing
   the page. Wraps the <ul.history-list>. */
.history-scroll {
  max-height: min(60vh, 560px);
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0 12px;
}

.history-scroll .history-row:last-child { border-bottom: none; }

/* Filter controls above the audit log - the free-text user filter plus
   the From/To date inputs, wrapping on a narrow viewport. */
.filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  margin-bottom: 12px;
}

.filter-row .admin-filter { margin-bottom: 0; width: 220px; }

.filter-row input[type="date"] {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 0.85rem;
  font-family: inherit;
}

.filter-row input[type="date"]:focus {
  outline: none;
  border-color: var(--accent-dim);
}

/* Users panel's "add member" row (admin/admin.js) - same input look as
   .field's above, just laid out inline instead of stacked since there's
   no label/description text needed per field here. */
.add-member-form {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.add-member-form input[type="text"],
.add-member-form select {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.85rem;
  font-family: inherit;
}

.users-table {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.85rem;
}

.users-table th {
  text-align: left;
  color: var(--text-dim);
  font-size: 0.78rem;
  font-weight: 400;
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}

.users-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
}

/* Admin.js/org.js append each row action (Remove, Reset password,
   Activate, Delete, ...) straight into the cell with appendChild - no
   wrapping flex container, so without this they render flush against
   each other with only incidental inline whitespace between them. */
.users-table td > button + button,
.users-table td > a + button,
.users-table td > button + a {
  margin-left: 8px;
}

/* Per-member service-access checkboxes (org.js Members panel + the
   add-member modal) - one per line, compact. */
.service-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  white-space: nowrap;
}

.service-toggle + .service-toggle {
  margin-top: 4px;
}

.audit-log-section {
  margin-top: 32px;
}

.audit-log-section h2 {
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin-bottom: 12px;
}

/* admin/ - unlike every other page, this one has no .wrap max-width; the
   sidebar+content shell uses the full viewport since panel tables (Phase 1+)
   need the room a 960px column doesn't give them. */
.admin-main { padding: 24px 24px 56px; }

.admin-shell {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 28px;
  align-items: start;
  max-width: 1400px;
  margin: 0 auto;
}

.admin-sidebar {
  display: flex;
  flex-direction: column;
  gap: 2px;
  position: sticky;
  top: 68px;
}

.admin-nav-item {
  padding: 8px 12px;
  border-radius: 6px;
  color: var(--text-dim);
  font-size: 0.88rem;
}

.admin-nav-item:hover { background: rgba(255, 255, 255, 0.06); color: var(--text); }

/* A .admin-nav-item that's an action <button> (org/'s "Contact support",
   which opens the compose modal) rather than an <a> panel link - reset the
   native button chrome so it sits flush with the anchor items above it. */
.admin-nav-action {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  font: inherit;
  font-size: 0.88rem;
  cursor: pointer;
}

.admin-nav-item.active {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}

/* Non-clickable heading above a group of nav items sharing one area (DNP3's
   four subpages below) - same micro-label look .admin-content h2 already
   uses for in-page section headings, so a group of related sidebar items
   reads the same way a group of related fields on a page does. */
.admin-nav-group-label {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  padding: 14px 12px 2px;
}

.admin-nav-subitem {
  padding-left: 22px;
}

/* Separates the cross-area links (admin-shell.js's renderShellNav -
   Super admin/Organization users/Manage account, whichever apply to the
   signed-in identity) and the trailing Sign out link from each page's
   own real sub-nav, same role a visual rule plays in
   widgets/topbar.js's own dropdown. */
.admin-nav-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 8px 0;
}

/* Floating "send a message" button (admin-shell.js's renderContactButton)
   - fixed to the viewport, not the page, so it stays put through scroll
   on every sidebar-shell page (/admin/, /org/, /account/). z-index above
   .admin-sidebar's own sticky positioning (top: 68px in normal flow) but
   below .popup-backdrop (the compose modal it opens sits on top of it). */
.shell-contact-fab {
  position: fixed;
  right: 24px;
  bottom: 24px;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: none;
  background: var(--accent);
  color: #04120d;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
  z-index: 15;
}

.shell-contact-fab:hover { background: var(--accent-dim); }

/* Scrollable, filterable list of orgs (admin.js's renderOrgPicker) - a
   plain <select> stops being practical to search once there are enough
   organizations to scroll through it blind; this stays a type-ahead over
   a visibly short list at any count. Rows reuse .admin-nav-item's own
   look (below), same "selectable item in a list" affordance the sidebar
   itself uses. */
.org-picker-list {
  max-height: 240px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 4px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-top: 8px;
}

.org-picker-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
}

.org-picker-empty {
  padding: 8px 12px;
}

/* Usage panel (admin.js's renderUsagePanel) - a hand-drawn SVG bar chart,
   no charting library (this site has no build step - see i18n.js's own
   header comment), following the dataviz skill's bar-chart spec: capped
   thin bars, a 2px surface gap between them, hairline recessive
   gridlines, single-series so no legend box. */
.usage-panel {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  margin-top: 20px;
}

.usage-presets {
  display: flex;
  gap: 8px;
  margin-bottom: 20px;
}

.usage-stat {
  font-family: var(--mono);
  font-size: 1.3rem;
  font-weight: 700;
  margin: 0 0 14px;
}

.usage-stat .dim {
  font-size: 0.82rem;
  font-weight: 400;
  margin-left: 10px;
  font-family: var(--sans);
}

/* Wide content (many daily bars) scrolls its own axis, same convention
   the admin portal's own tables already use - the bars stay at a fixed,
   spec-compliant thickness instead of being squeezed illegibly thin. */
.usage-chart-scroll {
  overflow-x: auto;
}

.usage-bar {
  fill: var(--accent);
}

.usage-bar.hovered {
  fill: var(--accent-dim);
}

.usage-bar-hit {
  fill: transparent;
  cursor: pointer;
}

.usage-gridline {
  stroke: var(--border);
  stroke-width: 1;
}

.usage-axis-label {
  fill: var(--text-dim);
  font-family: var(--mono);
  font-size: 9px;
}

.usage-tooltip {
  position: absolute;
  pointer-events: none;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 0.78rem;
  white-space: nowrap;
  transform: translate(-50%, -100%);
  z-index: 5;
}

.usage-tooltip[hidden] { display: none; }

.usage-tooltip .usage-tooltip-value { color: var(--text); font-weight: 700; font-family: var(--mono); }
.usage-tooltip .usage-tooltip-date { color: var(--text-dim); margin-top: 2px; }

.usage-table-toggle {
  margin-top: 14px;
  color: var(--text-dim);
  font-size: 0.82rem;
  cursor: pointer;
}

.usage-table-toggle table {
  margin-top: 10px;
}

.admin-content h1 {
  font-size: 1.3rem;
  margin: 8px 0 20px;
}

@media (max-width: 720px) {
  .admin-shell { grid-template-columns: 1fr; }
  .admin-sidebar { position: static; flex-direction: row; flex-wrap: wrap; }
}

.admin-content h2 {
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-dim);
  margin: 28px 0 12px;
}

/* Admin popups (Add member, Edit quota) - this site has
   no build step/framework (see i18n.js's own header comment), so this
   is the plain-CSS equivalent of the .popup-backdrop/.popup pattern
   computingsphere_dnp3's Vue frontend already uses (CapturesPage.vue,
   ImportCaptureModal.vue) - same look, ported here since nothing like
   it existed on this site before (raw window.prompt() stood in for it). */
.popup-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 20;
}

.popup {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 20px;
  width: 340px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* signup.js's openSignupModal - wider than the default .popup since the
   "professional portal" onboarding form (name/phone/job title/address,
   on top of email + org name) has far more fields than any other popup
   in this app. */
.signup-popup {
  width: 460px;
  max-width: calc(100vw - 32px);
}

.popup h2 {
  font-size: 0.95rem;
  margin: 0;
  color: var(--text);
}

.popup-form {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.popup-row {
  display: flex;
  gap: 8px;
}

.popup-row > * {
  flex: 1;
  min-width: 0;
}

.popup-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}

.popup .error-note {
  font-size: 0.8rem;
  color: #ff6b6b;
  margin: 0;
}

/* openConfirmDialog (admin-shell.js) - replaces window.confirm() for
   destructive actions (announcement delete, ...). Reuses .popup/
   .popup-backdrop/.popup-actions as-is, just a message instead of a form. */
.confirm-message {
  margin: 0;
  font-size: 0.85rem;
  color: var(--text);
  line-height: 1.5;
}

/* Type-to-confirm speed bump for irreversible actions (verify option of
   openConfirmDialog) - a labelled text input between message and buttons. */
.confirm-verify {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.8rem;
  color: var(--text-dim);
}

.confirm-verify input {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.95rem;
  font-family: inherit;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.field select {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.9rem;
  font-family: inherit;
}

/* Type-to-narrow filter above a panel's table/select (Users,
   Organizations, Credits, Audit Log) - the "doesn't scale past a
   handful of orgs/users" fix, client-side since none of these lists are
   large enough yet to need real server-side pagination/search. */
.admin-filter {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 7px 10px;
  font-size: 0.85rem;
  font-family: inherit;
  margin-bottom: 12px;
  width: 260px;
  max-width: 100%;
}

.admin-filter:focus {
  outline: none;
  border-color: var(--accent-dim);
}

/* Users panel's "Add member" button sits above the filter/table, its own
   row rather than crammed into <h1> - the popup itself carries the form
   now (openAddMemberModal), this is just the trigger. `gap` covers a
   toolbar that ever grows a second button (only one today per panel) -
   same reasoning as the .users-table button spacing above. */
.admin-toolbar {
  display: flex;
  gap: 8px;
  margin: 4px 0 16px;
}

/* Dense button standard for the admin-style portals (/admin/, /org/ -
   both render into .admin-content). The marketing-page .btn-primary/
   .btn-ghost/.btn-danger above (line ~57) are deliberately larger - CTA
   buttons meant to stand out on their own - but a table full of Remove/
   Reset password/Activate/Delete actions at that same size reads as
   oversized for what's a dense, repeated, secondary action. Proportions
   match computingsphere_dnp3's own toolbar buttons
   (frontend/src/pages/ProtocolPage.vue's .btn-secondary) so both apps'
   admin-style surfaces read as one consistent field instead of two. */
.admin-content .btn-primary,
.admin-content .btn-ghost,
.admin-content .btn-danger {
  padding: 6px 14px;
  font-size: 0.82rem;
  font-weight: 500;
  /* Tighter floor than the 120px base - dense, repeated table/toolbar
     actions, but still a shared width so Restart/Stop/Delete and the
     row-action buttons align. */
  min-width: 84px;
}

/* org.js's SCADA "Restart" button - unlike Start/Stop/Delete, it has no
   confirm dialog closing to signal "something happened", and disabling
   every toolbar button on click (action()'s own doing) is easy to miss -
   nothing visibly *changes*, inviting a second/third click. Swapping the
   icon for a spinner while the request is in flight is the same
   visible-latency signal computingsphere_scada's own CommunicationView.vue
   already uses (.comm-btn-spinner) for an analogous fire-and-forget
   toggle, just ported to this app's plain-CSS button set. */
.btn-spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, currentColor 35%, transparent);
  border-top-color: currentColor;
  animation: btn-spin 0.7s linear infinite;
}

@keyframes btn-spin {
  to { transform: rotate(360deg); }
}

/* Announcements panel's "Show on" service picker (openCreateAnnouncementModal) -
   overrides .popup-row's flex:1 on children, which would stretch a single
   checkbox+label to the full row width. */
.admin-checkbox-label {
  display: flex !important;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto !important;
  font-size: 0.85rem;
  cursor: pointer;
}

/* verify.html - the standalone landing page an emailed confirmation
   link opens. verify.js opens signup.js's openWelcomeModal as an
   overlay on success, so this card underneath only ever shows a
   "confirming..." message or an error + a way back to sign up again. */
.verify-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.verify-card {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 32px;
  width: 360px;
  max-width: 100%;
  text-align: center;
}

.verify-card #verify-retry {
  display: inline-block;
  margin-top: 12px;
  text-decoration: none;
}
