/* ============================================================================
   Islandr — app-specific layout
   The token + component CSS is the source of truth for visuals.
   This file is for things the design system does not own:
   page chrome, view-level grids, app-shell placement.
   ============================================================================ */

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--canvas);
  color: var(--fg1);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--lh-normal);
}

#app {
  min-height: 100%;
}

/* --- centered card layout for login -------------------------------------- */
.center-card-page {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--space-6);
  position: relative;
  isolation: isolate;
}

/* The canvas sits behind the card and covers the page. `isolation` keeps its
   negative z-index inside this stacking context rather than sliding under the
   app background. */
/* The meteor layer keeps the same z-index as the sky and wins on DOM order
   alone: two negative-z-index siblings paint in document order, and both stay
   behind the card. Lifting it to 0 would have put it in front of the card,
   because a positioned element at 0 paints above static content. */
.login-space {
  position: absolute;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Night scene (dark theme). The sign-in screen is the one place allowed more
   ambient motion than the brief's single hub pulse: nothing on it is data a
   moving background could distract from, and it is the same scene as the
   marketing site's hero. All of it stops under prefers-reduced-motion. */
.login-night {
  position: absolute;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
}
.login-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* Drawn once; the compositor turns the finished bitmap, one turn in 12 min. */
.login-sky {
  position: absolute;
  transform-origin: 50% 50%;
  will-change: transform;
  animation: login-sky-turn 720s linear infinite;
}
.login-sky canvas { display: block; width: 100%; height: 100%; }
@keyframes login-sky-turn { to { transform: rotate(-360deg); } }
.login-twinkle {
  position: absolute;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: #DDF4FB;
  box-shadow: 0 0 4px 1px rgba(170, 225, 245, 0.55);
  animation: login-twinkle var(--tw, 4s) ease-in-out infinite alternate;
}
@keyframes login-twinkle { from { opacity: 0.25; } to { opacity: 0.95; } }
.login-routes path {
  fill: none;
  stroke: rgba(111, 211, 232, 0.28);
  stroke-width: 1.2;
  stroke-linecap: round;
  stroke-dasharray: 2 7;
  animation: login-flow 5s linear infinite;
}
@keyframes login-flow { to { stroke-dashoffset: -36; } }
.login-light {
  position: absolute;
  width: 3px;
  height: 3px;
  margin: -1.5px 0 0 -1.5px;
  border-radius: 50%;
  background: #6FD3E8;
  box-shadow: 0 0 4px rgba(111, 211, 232, 0.6);
}
.login-light--hub {
  width: 4px;
  height: 4px;
  margin: -2px 0 0 -2px;
  background: #9FECF8;
  box-shadow: 0 0 6px rgba(159, 236, 248, 0.8);
}
.login-light-glow {
  position: absolute;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(159, 236, 248, 0.55), transparent 70%);
  animation: login-light-glow 1.8s ease-in-out infinite alternate;
}
@keyframes login-light-glow {
  from { opacity: 0.6; transform: scale(1); }
  to   { opacity: 1;   transform: scale(1.3); }
}
.login-reflection {
  position: absolute;
  width: 3px;
  margin-left: -1.5px;
  background: repeating-linear-gradient(to bottom, rgba(111, 211, 232, 0.35) 0 2px, transparent 2px 5px);
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent);
          mask-image: linear-gradient(to bottom, #000, transparent);
  animation: login-shimmer 3.2s ease-in-out infinite alternate;
}
@keyframes login-shimmer {
  from { opacity: 0.35; transform: scaleX(1); }
  to   { opacity: 0.8;  transform: scaleX(2.2); }
}
@media (prefers-reduced-motion: reduce) {
  .login-sky, .login-twinkle, .login-routes path,
  .login-light-glow, .login-reflection { animation: none; }
}

/* The wordmark had a fixed dark fill and nearly vanished on the night sky. */
.login-lockup-word { fill: #172B3A; }
[data-theme="dark"] .login-lockup-word { fill: var(--fg1); }

/* The lockup pulse. The topology hub pulse holds the
   budget on the dashboard, and the two never share a view — the sign-in screen
   has no topology on it. Same two-layer glow-pulse as the hub node on the
   marketing site's hero constellation (islandr-gateway.net) — same 1.8s,
   same ease-in-out, same alternate direction, same reduced-motion exit —
   so the mark reads as the same brand animation wherever it appears. */
.login-lockup-pulse-outer {
  transform-origin: 64px 27px;
  animation: login-lockup-glow-outer 1.8s ease-in-out infinite alternate;
}
.login-lockup-pulse-inner {
  transform-origin: 64px 27px;
  animation: login-lockup-glow-inner 1.8s ease-in-out infinite alternate;
}

@keyframes login-lockup-glow-inner {
  from { opacity: 0.9; transform: scale(1); }
  to   { opacity: 1;   transform: scale(1.18); filter: drop-shadow(0 0 6px #9FECF8); }
}
@keyframes login-lockup-glow-outer {
  from { opacity: 0.22; transform: scale(1); }
  to   { opacity: 0.38; transform: scale(1.22); }
}

@media (prefers-reduced-motion: reduce) {
  .login-lockup-pulse-outer,
  .login-lockup-pulse-inner { animation: none; }
}


.center-card {
  width: 100%;
  max-width: 400px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--space-8);
}

.wordmark {
  font-family: var(--font-mono);
  font-size: var(--text-xl);
  font-weight: 500;
  color: var(--fg1);
  letter-spacing: -0.01em;
  text-align: center;
  margin-bottom: var(--space-6);
}

.wordmark .accent {
  color: var(--accent-500);
}

.center-card h1 {
  font-size: var(--text-lg);
  font-weight: 600;
  margin: 0 0 var(--space-6) 0;
  text-align: center;
  color: var(--fg1);
}

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.field label {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--fg2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.btn-block {
  width: 100%;
}

/* --- app shell for authenticated views (sidebar + topbar + main) -------- */
.app-shell {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: 56px 1fr;
  grid-template-areas:
    "sidebar topbar"
    "sidebar main";
  min-height: 100vh;
}

.topbar {
  grid-area: topbar;
  height: 56px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  padding: 0 var(--space-6);
  gap: var(--space-6);
}

.topbar .spacer {
  flex: 1;
}

.sidebar {
  grid-area: sidebar;
  background: var(--surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: var(--space-5) 0 var(--space-4) 0;
  gap: var(--space-2);
  /* Stay in view on long pages: pin to the viewport and keep the full height so
     the footer (Settings) is always reachable without scrolling to the bottom
     of the page. The nav scrolls inside the rail if it ever exceeds the height. */
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
}

.sidebar .wordmark {
  margin: 0 0 var(--space-5) 0;
  padding: 0 var(--space-5);
  font-size: var(--text-md);
  text-align: left;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.sidebar .wordmark svg {
  flex-shrink: 0;
  display: block;
}

.sidebar .section-label {
  padding: var(--space-4) var(--space-5) var(--space-2) var(--space-5);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--fg3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.sidebar .nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: 36px;
  padding: 0 var(--space-5);
  color: var(--fg2);
  text-decoration: none;
  font-size: var(--text-sm);
  font-weight: 500;
  border-left: 2px solid transparent;
  /* Hover and active existed; the transition did not, so the background
     snapped between them. That single missing line is most of what makes an
     interface feel unfinished next to a desktop application. */
  transition:
    background var(--dur-fast) var(--ease-standard),
    color var(--dur-fast) var(--ease-standard),
    border-left-color var(--dur) var(--ease-standard);
  transition: background var(--dur) var(--ease-standard), color var(--dur) var(--ease-standard);
  position: relative;
}

.sidebar .nav-item:hover {
  background: var(--surface-2);
  color: var(--fg1);
}

.sidebar .nav-item.router-link-active {
  background: var(--accent-soft-bg);
  color: var(--accent);
  border-left-color: var(--accent);
}

/* Press feedback, the same 0.5px the buttons use — consistency is the point,
   not the pixel. And one focus treatment, so the ring does not differ per
   component. */
.sidebar .nav-item:active {
  transform: translateY(0.5px);
}

.sidebar .nav-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-md);
}

/* The icon answers alongside the colour. One property changing on its own is
   what reads as "web page"; an application tends to reply in more than one
   channel at once. */
.sidebar .nav-item svg {
  opacity: 0.75;
  transition: opacity var(--dur-fast) var(--ease-standard);
}

.sidebar .nav-item:hover svg,
.sidebar .nav-item.router-link-active svg {
  opacity: 1;
}

.sidebar .nav-item .count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg3);
}

.sidebar .nav-item.router-link-active .count {
  color: var(--accent);
}

/* Lucide-style icons in the sidebar. width/height are set by the <Icon size>
   prop; this rule just colours them and gives them their pre-hover hue. */
.sidebar .nav-item .icon {
  flex-shrink: 0;
  color: var(--fg3);
}

.sidebar .nav-item.router-link-active .icon,
.sidebar .nav-item:hover .icon {
  color: inherit;
}

/* Fallback marker for an Icon name that's not in the library — easier to spot
   in dev than a silently missing glyph. */
.icon-missing {
  display: inline-block;
  border: 1.5px dashed var(--status-warning, #b07b00);
  border-radius: 2px;
}

.sidebar .sidebar-footer {
  margin-top: auto;
  border-top: 1px solid var(--border);
  padding-top: var(--space-2);
}

.main {
  grid-area: main;
  padding: var(--space-8);
  max-width: 1600px;
  width: 100%;
  margin: 0 auto;
  /* Grid items default to min-width:auto and won't shrink below their content's
     min-content width, so any wide/unbreakable content could stretch the track
     past the viewport. Let the track win; inner scroll boxes handle the rest. */
  min-width: 0;
}

@media (min-width: 1800px) {
  .main {
    max-width: 1760px;
  }
}

@media (max-width: 720px) {
  .app-shell {
    grid-template-columns: 1fr;
    grid-template-rows: 56px auto 1fr;
    grid-template-areas:
      "topbar"
      "sidebar"
      "main";
  }
  .sidebar {
    flex-direction: row;
    flex-wrap: wrap;
    padding: var(--space-2);
    border-right: 0;
    border-bottom: 1px solid var(--border);
    /* Horizontal bar on mobile — undo the desktop sticky full-height rail. */
    position: static;
    height: auto;
    overflow-y: visible;
  }
  .sidebar .wordmark,
  .sidebar .section-label {
    display: none;
  }
  /* The footer holds Settings and My access — two ways in, not decoration, so
     hiding it here left them reachable only by typing the URL. `contents`
     dissolves the wrapper so both links become direct children of the bar and
     pick up the .nav-item rule below, exactly like every other entry. It also
     drops the desktop `margin-top: auto` and the divider, which mean nothing
     in a horizontal row. A plain div has no semantics for `contents` to
     discard — the accessibility caveat applies to lists and tables. */
  .sidebar .sidebar-footer {
    display: contents;
  }
  .sidebar .nav-item {
    height: 32px;
    border-left: 0;
    border-radius: var(--radius-md);
  }
}

/* --- stub view (placeholder) -------------------------------------------- */
.stub {
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-12);
  text-align: center;
  color: var(--fg2);
}

.stub h2 {
  margin: 0 0 var(--space-2) 0;
  font-size: var(--text-lg);
  color: var(--fg1);
}

.stub p {
  margin: 0;
  max-width: 480px;
  margin: 0 auto;
}

.page-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-6);
}

.page-header h1 {
  font-size: var(--text-2xl);
  font-weight: 600;
  margin: 0;
  letter-spacing: -0.01em;
}

/* --- table tweaks for the users view ------------------------------------- */
.empty-state {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-12);
  text-align: center;
  color: var(--fg2);
}

.empty-state h2 {
  font-size: var(--text-lg);
  font-weight: 500;
  margin: 0 0 var(--space-2) 0;
  color: var(--fg1);
}

.empty-state p {
  margin: 0 0 var(--space-6) 0;
}

/* The constellation from the landing page, carried into the product. The empty
   state is the only generous free area here — thirteen views share this class,
   and a fresh installation consists of little else. Applied as a mask so it
   takes currentColor and follows the theme; a background-image cannot, and the
   first attempt drew it in the SVG's default fill, which is black, and was
   therefore invisible on the night canvas. */
.empty-state {
  position: relative;
  padding-top: calc(var(--space-12) + 64px);
}

.empty-state::before {
  content: "";
  position: absolute;
  top: var(--space-8);
  left: 50%;
  transform: translateX(-50%);
  width: 108px;
  height: 76px;
  background-color: currentColor;
  opacity: 0.16;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20240%20168%22%3E%3Cpath%20d%3D%22M30%20150L90%20124L130%2074L198%2036M130%2074L112%2026%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%22150%22%20r%3D%224.5%22%2F%3E%3Ccircle%20cx%3D%2290%22%20cy%3D%22124%22%20r%3D%224%22%2F%3E%3Ccircle%20cx%3D%22130%22%20cy%3D%2274%22%20r%3D%225.5%22%2F%3E%3Ccircle%20cx%3D%22198%22%20cy%3D%2236%22%20r%3D%224.5%22%2F%3E%3Ccircle%20cx%3D%22112%22%20cy%3D%2226%22%20r%3D%228%22%2F%3E%3C%2Fsvg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20240%20168%22%3E%3Cpath%20d%3D%22M30%20150L90%20124L130%2074L198%2036M130%2074L112%2026%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%222.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Ccircle%20cx%3D%2230%22%20cy%3D%22150%22%20r%3D%224.5%22%2F%3E%3Ccircle%20cx%3D%2290%22%20cy%3D%22124%22%20r%3D%224%22%2F%3E%3Ccircle%20cx%3D%22130%22%20cy%3D%2274%22%20r%3D%225.5%22%2F%3E%3Ccircle%20cx%3D%22198%22%20cy%3D%2236%22%20r%3D%224.5%22%2F%3E%3Ccircle%20cx%3D%22112%22%20cy%3D%2226%22%20r%3D%228%22%2F%3E%3C%2Fsvg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
  pointer-events: none;
}

.error-banner {
  background: var(--danger-bg);
  border: 1px solid var(--danger-border);
  color: var(--danger-fg);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
}

.muted {
  color: var(--fg3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* --- warning callout (setup-incomplete, one-time-secret banner) ---------- */
.callout {
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  font-size: var(--text-sm);
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

/* Flex children default to min-width:auto and so won't shrink below their
   content's min-content width. Without this, a long unbreakable line inside a
   callout (e.g. the install commands in the enforcement banner) stretches the
   whole banner and overflows the page instead of scrolling inside its own
   overflow-x:auto box. */
.callout > * {
  min-width: 0;
}

.callout-warning {
  background: var(--warning-bg);
  border: 1px solid var(--warning-border);
  color: var(--warning-fg);
}

/* Success variant — used by the provider configuration test and the
   admin-consent return (#81). Status still reads as icon + word; the colour
   only reinforces it. */
.callout-ok {
  background: var(--success-bg);
  border: 1px solid var(--success-border);
  color: var(--success-fg);
}

.callout-info {
  background: var(--info-bg);
  border: 1px solid var(--info-border);
  color: var(--info-fg);
}

.callout strong {
  font-weight: 600;
}

.callout a {
  color: inherit;
  text-decoration: underline;
}

/* --- modal --------------------------------------------------------------- */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 32, 0.45);
  display: grid;
  place-items: center;
  padding: var(--space-6);
  z-index: 1000;
}

.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md, 0 10px 30px rgba(0, 0, 0, 0.18));
  width: 100%;
  max-width: 560px;
  max-height: calc(100vh - 96px);
  /* Header and footer stay pinned; only the body scrolls when content is tall,
     so action buttons in the footer are always reachable. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.modal-lg {
  max-width: 720px;
}

.modal-xl {
  max-width: 1020px;
  min-width: min(860px, 95vw);
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-5) var(--space-6);
  border-bottom: 1px solid var(--border);
  flex: none;
}

.modal-header h2 {
  font-size: var(--text-lg);
  font-weight: 600;
  margin: 0;
}

.modal-body {
  padding: var(--space-6);
  overflow-y: auto;
  flex: 1 1 auto;
  min-height: 0;
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-6);
  border-top: 1px solid var(--border);
  flex: none;
}

/* A destructive action in a footer sits apart from confirm/cancel, so it is
   never the button the hand lands on by reflex. */
.modal-footer-left {
  margin-right: auto;
}

/* Several modals wrap .modal-body + .modal-footer in a <form> so Enter
   submits (modal-header stays outside, as a sibling, for its own close
   button). That form is a plain block box, not a flex item — it broke the
   .modal flex chain, so .modal-body's flex:1 1 auto / overflow-y:auto never
   applied. The body rendered at full content height instead of shrinking
   to fit, .modal's own overflow:hidden then clipped the excess with no
   scrollbar at all, and the footer (with the submit button) ended up
   unreachable on any form tall enough to not fit the viewport — e.g. the
   Create Peer modal. display:contents removes the <form>'s own box from
   layout so modal-body/modal-footer become direct flex children of .modal
   again, without changing its submit-on-enter behavior. */
.modal > form {
  display: contents;
}

/* --- key-import mode (radio group inside peer-create modal) ------------- */
.key-mode {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.key-mode legend {
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--fg2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0 var(--space-2);
}

.key-mode-option {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--dur) var(--ease-standard);
}

.key-mode-option:hover {
  background: var(--surface-2);
}

.key-mode-option input[type="radio"] {
  margin-top: 3px;
  accent-color: var(--accent);
}

.key-mode-title {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--fg1);
  display: flex;
  align-items: center;
}

.key-mode-hint {
  font-size: var(--text-xs);
  color: var(--fg3);
  margin-top: 2px;
  line-height: 1.45;
}

/* --- one-time-secret block (QR + .conf) ---------------------------------- */
.secret-block {
  display: grid;
  grid-template-columns: 220px minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
  margin-top: var(--space-4);
}

.secret-block > * {
  min-width: 0;
}

.secret-block .qr {
  width: 220px;
  height: 220px;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-2);
  display: grid;
  place-items: center;
  flex-shrink: 0;
}

.secret-block .qr img {
  width: 100%;
  height: 100%;
  image-rendering: pixelated;
}

.conf-block {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  white-space: pre;
  overflow-x: auto;
  overflow-y: auto;
  max-height: 320px;
  color: var(--fg1);
}

.secret-block-no-qr {
  grid-template-columns: 1fr;
}

@media (max-width: 640px) {
  .secret-block {
    grid-template-columns: 1fr;
  }
  .secret-block .qr {
    width: 100%;
    height: auto;
    aspect-ratio: 1;
    margin: 0 auto;
    max-width: 240px;
  }
}

/* --- expandable user row (peers list under each user) ------------------- */
.subrow td {
  background: var(--surface-2);
  padding: var(--space-4) var(--space-4) var(--space-5) var(--space-8) !important;
  height: auto !important;
}

.subrow .peers-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.subrow .peer-row {
  display: grid;
  grid-template-columns: 1.5fr 1fr 1fr auto auto;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

.subrow .peer-row .mono {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--fg2);
}

/* --- two-column form grid for SettingsView ------------------------------- */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4) var(--space-5);
}

.form-grid .field {
  margin-bottom: 0;
}

.form-grid .field-full {
  grid-column: 1 / -1;
}

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

.field-hint {
  font-size: var(--text-xs);
  color: var(--fg3);
}

/* --- avatar -------------------------------------------------------------- */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  overflow: hidden;
  flex-shrink: 0;
  font-family: var(--font-sans);
  font-weight: 600;
  letter-spacing: 0.02em;
  -webkit-user-select: none;
  user-select: none;
}
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.avatar-initials {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  text-transform: uppercase;
}

/* Editable avatar (issue #85). The pencil is the one permanent control;
   removing a picture is one tap behind it (menu below), not a second icon
   that used to sit next to the avatar on every touch device — there is no
   hover there to reveal it on, so @media (hover: none) used to show it
   permanently, right where this task's whole point was to stop that. */
.avatar-edit-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.avatar-edit-btn {
  position: relative;
  display: inline-flex;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  border-radius: 999px;
  transition: transform 120ms cubic-bezier(0.2, 0, 0.2, 1);
}

.avatar-edit-btn:disabled {
  cursor: progress;
  opacity: 0.6;
}

.avatar-edit-btn:active:not(:disabled) {
  transform: translateY(0.5px);
}

.avatar-edit-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.avatar-edit-badge {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 14px;
  height: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  line-height: 1;
  border-radius: 999px;
  background: var(--surface);
  /* --border alone sits too close to --surface in dark mode to read as an
     edge against an arbitrary photo behind it — --border-strong plus a
     shadow keeps the badge legible regardless of what it's floating over. */
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-xs);
  color: var(--fg2);
}

/* The avatar in the topbar is there to say who you are, not to be edited. The
   pencil badge sat on it permanently, which put standing emphasis on an
   action taken about once a year. It fades in on hover or keyboard focus
   instead.

   Not hover-only, which the brief rules out and which this component had
   avoided on purpose: an element at opacity 0 is unreachable on a touch device
   and still takes tab focus, so the ring would land on something invisible.
   `:focus-within` answers the second, the hover query below the first. It
   keeps its layout space, so nothing shifts when it appears. */
.avatar-edit-wrap .avatar-edit-badge {
  opacity: 0;
  transition: opacity 150ms cubic-bezier(0.2, 0, 0.2, 1);
}

.avatar-edit-wrap:hover .avatar-edit-badge,
.avatar-edit-wrap:focus-within .avatar-edit-badge {
  opacity: 1;
}

/* No hover to reveal it with — showing it permanently is the only way it
   exists at all here. */
@media (hover: none) {
  .avatar-edit-wrap .avatar-edit-badge {
    opacity: 1;
  }
}

/* The two-item "change / remove" menu the pencil opens once a picture
   exists. Anchored to .avatar-edit-wrap (position: relative above), so it
   floats below the avatar regardless of where that sits in the page — a
   32px row in a dense user list and a 28px topbar corner both work without
   a bespoke offset for each. */
.avatar-edit-menu {
  position: absolute;
  top: calc(100% + var(--space-1));
  left: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 140px;
  padding: var(--space-1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
}

.avatar-edit-menu-item {
  display: block;
  width: 100%;
  padding: var(--space-2) var(--space-2);
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  text-align: left;
  color: var(--fg1);
  cursor: pointer;
}

.avatar-edit-menu-item:hover:not(:disabled) {
  background: var(--surface-2);
}

.avatar-edit-menu-item:disabled {
  cursor: progress;
  opacity: 0.6;
}

.avatar-edit-menu-item-danger {
  color: var(--danger-fg);
}

.avatar-edit-menu-item-danger:hover:not(:disabled) {
  background: var(--danger-bg);
}

/* --- theme toggle button in the topbar ----------------------------------- */
.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  padding: 0;
}
.topbar-lang-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  color: var(--fg3);
  font-size: var(--text-xs);
}
.topbar-lang-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--fg3);
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  transition: color 120ms;
  line-height: 1;
}
.topbar-lang-btn:hover { color: var(--fg1); }
.topbar-lang-btn.active { color: var(--accent-500); font-weight: 600; }

/* --- topbar user chip ---------------------------------------------------- */
.topbar-user {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--fg2);
  font-size: var(--text-sm);
  margin-right: var(--space-3);
}
.topbar-user-name {
  font-weight: 500;
}

/* --- login OAuth buttons -------------------------------------------------- */
.oauth-divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-4) 0;
  color: var(--fg3);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.oauth-divider::before,
.oauth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}
.oauth-btn {
  margin-top: var(--space-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
}
.oauth-mark {
  width: 16px;
  height: 16px;
  display: inline-grid;
  flex-shrink: 0;
}
.oauth-mark--ms {
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 1px;
}
.oauth-mark--ms span:nth-child(1) { background: #F25022; }
.oauth-mark--ms span:nth-child(2) { background: #7FBA00; }
.oauth-mark--ms span:nth-child(3) { background: #00A4EF; }
.oauth-mark--ms span:nth-child(4) { background: #FFB900; }
.oauth-mark--google {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: 2px;
  font-family: var(--font-sans);
  font-weight: 700;
  font-size: 12px;
  color: #4285F4;
  line-height: 1;
}

/* --- identity provider cards --------------------------------------------- */
.identity-providers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
  gap: var(--space-5);
}
.identity-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
}
.identity-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
}
.identity-card-head h2 {
  margin: 0;
  font-size: var(--text-md);
}
.code-block {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  overflow-x: auto;
  margin: 0 0 var(--space-2);
}
.card-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
}

/* --- toggle (checkbox + label) ------------------------------------------- */
.toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}
.toggle input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
}

/* --- login card: provider hierarchy ------------------------------------- */
.login-card {
  max-width: 440px;
  padding: var(--space-8);
}
.login-lockup-wrap {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.login-lockup {
  display: block;
  width: 180px;
  height: 48px;
  flex-shrink: 0;
  overflow: visible;
}
.login-product-name {
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: 300;
  color: var(--fg2);
  letter-spacing: 0.01em;
  padding-left: var(--space-1);
  border-left: 1px solid var(--border);
  margin-left: var(--space-1);
  line-height: 1;
  padding-top: 2px;
}
.login-tagline {
  text-align: center;
  font-size: var(--text-xs);
  color: var(--fg3);
  margin: 0 0 var(--space-6);
  letter-spacing: 0.01em;
}
.login-footer-legal {
  text-align: center;
  font-size: var(--text-xs);
  color: var(--fg3);
  margin-top: var(--space-5);
}
.login-lang-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  margin-top: var(--space-6);
}
.login-lang-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--text-xs);
  font-weight: 500;
  color: var(--fg3);
  padding: 2px 4px;
  border-radius: var(--radius-sm);
  transition: color 120ms;
}
.login-lang-btn:hover { color: var(--fg1); }
.login-lang-btn.active { color: var(--accent-500); font-weight: 600; }
.login-lang-sep { color: var(--fg3); font-size: var(--text-xs); }
.login-title {
  font-size: var(--text-lg);
  font-weight: 600;
  text-align: center;
  margin: 0 0 var(--space-2);
}
.login-subtitle {
  text-align: center;
  color: var(--fg3);
  font-size: var(--text-sm);
  margin: 0 0 var(--space-6);
}

/* Primary OAuth button — surface-coloured, full-width, logo + text */
.oauth-btn-primary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  background: var(--surface);
  color: var(--fg1);
  border: 1px solid var(--border);
  font-weight: 500;
  font-size: var(--text-base);
  height: 44px;
  border-radius: var(--radius-md);
  transition: background 160ms cubic-bezier(0.2,0,0.2,1),
              border-color 160ms cubic-bezier(0.2,0,0.2,1),
              transform 120ms cubic-bezier(0.2,0,0.2,1);
}
.oauth-btn-primary:hover {
  background: var(--surface-2);
  border-color: var(--border-strong, var(--fg3));
}
.oauth-btn-primary:active {
  transform: translateY(0.5px);
}
.oauth-btn-primary .oauth-mark {
  width: 18px;
  height: 18px;
}

/* Divider with "oder" / "Lokal" label in the middle */
.login-divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-5) 0 var(--space-4);
  color: var(--fg3);
  font-size: var(--text-xs);
}
.login-divider::before,
.login-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* Secondary "Mit lokalem Konto anmelden" toggle — text-button, sits under divider */
.login-local-toggle {
  display: block;
  width: 100%;
  background: transparent;
  border: none;
  color: var(--fg2);
  font: inherit;
  font-size: var(--text-sm);
  padding: var(--space-2);
  cursor: pointer;
  text-align: center;
  border-radius: var(--radius-sm);
  transition: color 120ms, background 120ms;
}
.login-local-toggle:hover {
  color: var(--fg1);
  background: var(--surface-2);
}

.login-local-form {
  margin-top: var(--space-2);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.login-local-form .field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}
.login-local-form .field label {
  font-size: var(--text-sm);
  color: var(--fg2);
  font-weight: 500;
}
.login-local-form .btn {
  margin-top: var(--space-2);
  height: 44px;
}

/* --- identity page ------------------------------------------------------ */
.page-sub {
  margin: var(--space-1) 0 0;
  color: var(--fg3);
  font-size: var(--text-sm);
}

/* Eyebrow label (tiny ALL-CAPS) — design system reserves ALL-CAPS for these */
.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: var(--text-xs);
  color: var(--fg3);
  font-weight: 600;
}

/* Logo capsule for provider marks */
.idp-logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  border-radius: var(--radius-md);
  flex-shrink: 0;
}
.idp-logo--sm  { width: 36px;  height: 36px; }
.idp-logo--lg  { width: 56px;  height: 56px; }
.idp-logo--xl  { width: 96px;  height: 96px; padding: 24px; }
.idp-logo--lg .oauth-mark { width: 28px; height: 28px; }
.idp-logo--xl .oauth-mark { width: 48px; height: 48px; font-size: 28px; }
.idp-logo--local { color: var(--fg2); }
/* Generic OIDC providers (Okta/Auth0/Keycloak/any issuer, issue #69) — plain
   neutral icon, not a colored brand mark. Unlike Microsoft/Google (consumer
   "Sign in with" kits explicitly permit third-party button use of their
   logo), these are B2B IdPs with no equivalent published permission — see
   docs/adr or the identity.md competitor notes for the reasoning. */
.idp-logo--custom { color: var(--fg2); }

/* Hero card */
.identity-hero {
  padding: var(--space-6);
  margin-bottom: var(--space-4);
}
.identity-hero-main {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-4);
  align-items: center;
}
.identity-hero-text h2 {
  margin: var(--space-1) 0 var(--space-1);
  font-size: var(--text-lg);
}
.identity-hero-meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-3) var(--space-5);
  margin: var(--space-5) 0 0;
  padding: var(--space-4) 0 0;
  border-top: 1px solid var(--border);
}
.identity-hero-meta dt {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg3);
  margin: 0 0 var(--space-1);
}
.identity-hero-meta dd {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--fg1);
  word-break: break-all;
}
.identity-hero-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
  flex-wrap: wrap;
}

.status-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

/* Empty-hero (Provider-Wahl beim Erstaufruf) */
.identity-hero--empty {
  text-align: center;
  padding: var(--space-7) var(--space-6);
}
.identity-hero--empty h2 {
  margin: var(--space-2) 0 var(--space-2);
}
.identity-hero--empty .muted {
  max-width: 56ch;
  margin: 0 auto var(--space-5);
}
.identity-choice-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-4);
  max-width: 520px;
  margin: 0 auto;
}
.identity-choice {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  text-align: center;
  transition: background 160ms cubic-bezier(0.2,0,0.2,1),
              border-color 160ms cubic-bezier(0.2,0,0.2,1),
              transform 120ms cubic-bezier(0.2,0,0.2,1);
}
.identity-choice:hover {
  background: var(--surface-2);
  border-color: var(--fg3);
}
.identity-choice:active { transform: translateY(0.5px); }
.identity-choice-label {
  font-size: var(--text-base);
  font-weight: 500;
  color: var(--fg1);
}
.identity-choice-hint {
  color: var(--accent-500);
}

/* Strip-Karten unter dem Hero */
.identity-strips {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-5);
}
.identity-strip {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: var(--space-3);
  align-items: center;
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.identity-strip-name {
  font-weight: 500;
  color: var(--fg1);
}
.identity-strip-actions {
  display: flex;
  gap: var(--space-2);
}
.identity-strip--local {
  background: var(--surface-2);
}
.identity-strip--add {
  background: transparent;
  border-style: dashed;
}

/* Small-width override for confirm-style dialogs. .modal/.modal-backdrop's
   base (scrollable body, pinned header/footer) is defined above — a second,
   conflicting .modal/.modal-backdrop declaration used to live here and
   dropped the max-height/overflow-hidden constraint from the base rule,
   which silently broke scrolling on every tall modal in the app (e.g. the
   Create Peer form's submit button becoming unreachable with no scrollbar,
   since .modal grew to fit its content instead of capping at the viewport
   and letting .modal-body scroll internally). */
.modal.modal-sm { max-width: 440px; }

@media (max-width: 640px) {
  .identity-hero-main {
    grid-template-columns: auto 1fr;
  }
  .identity-hero-status {
    grid-column: 1 / -1;
  }
  .identity-choice-grid {
    grid-template-columns: 1fr;
  }
}

/* --- topology diagram --------------------------------------------------- */
/* The one ambient motion permitted by CLAUDE.md: a soft pulse on the hub
   node, 1.8s loop, halts on prefers-reduced-motion. The actual color comes
   from CSS variables so it follows the theme. */
.topo {
  /* No ancestor gives this a definite height, so height:100% used to resolve
     to "auto" — the SVG then sized itself purely from width * viewBox aspect
     ratio, growing well past the viewport on wide screens. Cap it instead;
     the default preserveAspectRatio (xMidYMid meet) keeps content centered
     and undistorted within the box. */
  width: 100%;
  height: min(440px, 55vh);
  display: block;
}
.topo .hub-core {
  fill: var(--accent);
}
.topo .hub-pulse {
  fill: var(--accent);
  transform-origin: center;
  transform-box: fill-box;
  animation: topo-hub-pulse 1.8s ease-in-out infinite;
  opacity: 0.5;
}
@keyframes topo-hub-pulse {
  0%   { transform: scale(1);    opacity: 0.5; }
  50%  { transform: scale(1.6);  opacity: 0.0; }
  100% { transform: scale(1);    opacity: 0.5; }
}
@media (prefers-reduced-motion: reduce) {
  .topo .hub-pulse { animation: none; opacity: 0.3; }
}
/* edge-hub-entity (1.2.0): a disabled edge hub still shows on the world
   map (it still physically exists) but reads as inactive — same dimmed
   treatment a disabled peer gets elsewhere, never color-only (the label
   text itself already says "Disabled" wherever status is listed; here the
   hub has no status line of its own, so dimming is the only signal — kept
   deliberately strong (0.35) rather than subtle so it reads at a glance). */
.worldmap-hub-disabled {
  opacity: 0.35;
}
.worldmap-hub-disabled .hub-pulse {
  animation: none;
}
.topo .hub-mark {
  fill: var(--fg-on-accent);
  stroke: var(--fg-on-accent);
}
.topo .link {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.4;
  opacity: 0.5;
}
.topo .link-down   { opacity: 0.55; stroke-dasharray: 4 4; stroke: var(--fg3); }
.topo .link-live   { fill: none; stroke: var(--accent); stroke-width: 1; opacity: 0.3; stroke-dasharray: 2 3; }
/* Third link/node state (issue #34): a live peer whose byte counters moved
   between the last two live-polls, distinct from "connected but idle". Line
   thickness carries the traffic-volume signal — flowing-heavy is the
   thicker of the two — so the diagram reads at a glance without needing the
   byte-counter table underneath. No added motion — only color/weight/dash
   change, per the one-ambient-motion rule (the hub pulse already owns that
   budget). */
.topo .link-flowing       { stroke-width: 1.8; opacity: 0.7;  stroke-dasharray: none; }
.topo .link-flowing-heavy { stroke-width: 3;   opacity: 0.95; stroke-dasharray: none; }
.topo .live-dot-flowing { opacity: 1; }
.topo .node {
  cursor: pointer;
}
.topo .node-bg {
  fill: var(--surface);
  stroke: var(--border);
  stroke-width: 1;
}
.topo .node.live      .node-ring { stroke: var(--success, #16a34a); stroke-width: 2; }
.topo .node.disabled  .node-ring { stroke: var(--fg3);              stroke-width: 1.4; stroke-dasharray: 2 3; }
/* A network box is a CIDR grouping, not a device — no status to show, just
   a neutral border. The "expanded" accent highlight (networkRingStyle
   inline style) still overrides this — that's a selection state, not a
   status. */
.topo .node.network   .node-ring { stroke: var(--border); stroke-width: 1.4; }
/* topology-resource-ring: a resource has no connectivity check of its own —
   gateway liveness stands in (myaccess-reachability-indicator uses the same
   source). Stroke width matches the connecting line (1.4) in every state,
   same as before; only color/dash/opacity carry the status, and "down"
   additionally dims the icon+label below — status is never color alone.
   "unknown" (no gateway at this site) keeps the old neutral accent ring. */
.topo .node.resource.reachability-unknown .node-ring { stroke: var(--accent); stroke-width: 1.4; opacity: 0.55; }
.topo .node.resource.reachability-live    .node-ring { stroke: var(--success, #16a34a); stroke-width: 1.4; }
.topo .node.resource.reachability-down    .node-ring { stroke: var(--fg3); stroke-width: 1.4; stroke-dasharray: 3 3; }
.topo .node.resource.reachability-down    .node-icon { color: var(--fg3); }
.topo .node.resource.reachability-down    .node-label { fill: var(--fg3); }
/* topology-peer-detail-levels: tier 1/2 peer nodes — connected is a solid
   green ring (same convention as every other "live" node), stale dashes the
   ring and dims icon/label, same "status is never color alone" pattern as
   the resource ring above. A tier-2 dot (no node-bg/node-icon/node-label
   children) only needs the ring rule; tier-1's full node picks up the rest. */
.topo .node.peer.status-live  .node-ring { stroke: var(--success, #16a34a); stroke-width: 1.4; }
.topo .node.peer.status-stale .node-ring { stroke: var(--fg3); stroke-width: 1.4; stroke-dasharray: 3 3; }
.topo .node.peer.status-stale .node-icon { color: var(--fg3); }
.topo .node.peer.status-stale .live-label,
.topo .node.peer.status-stale .live-ip { fill: var(--fg3); }
/* Tier-2 orbit dot: status-live/-stale double as the fill too, since a bare
   <circle> here has no separate node-ring child to style. */
.topo circle.node.peer.status-live  { fill: var(--success, #16a34a); stroke: none; }
.topo circle.node.peer.status-stale { fill: var(--fg3); stroke: none; }
/* Tier-3 collapsed summary's distribution bar. */
.topo .peer-summary-bar-bg        { fill: var(--surface-2); }
.topo .peer-summary-bar-connected { fill: var(--success, #16a34a); }
.topo .peer-summary-bar-stale     { fill: var(--fg3); }
.topo .node-glyph {
  fill: none;
  stroke: var(--fg1);
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.topo .node.disabled .node-glyph { stroke: var(--fg3); }
/* Lucide-Pfade in den Resource-Knoten. stroke + line-Settings sind im SVG
   selbst gesetzt (siehe TopologyDiagram), hier nur Farbe via currentColor. */
.topo .node-icon { color: var(--fg1); pointer-events: none; }
.topo .node.disabled .node-icon { color: var(--fg3); }
.topo .node-label {
  fill: var(--fg2);
  font-family: var(--font-mono);
  font-size: 11px;
  text-anchor: middle;
  pointer-events: none;
}
.topo .node:hover .node-bg { fill: var(--surface-2); }
.topo .hub-label {
  fill: var(--fg2);
  font-family: var(--font-mono);
  font-size: 11px;
  text-anchor: middle;
  pointer-events: none;
}
.topo .hub-endpoint {
  fill: var(--fg3);
  font-family: var(--font-mono);
  font-size: 10px;
  text-anchor: middle;
  pointer-events: none;
}
.topo .live-label {
  fill: var(--fg2);
  font-family: var(--font-mono);
  font-size: 10px;
  text-anchor: middle;
  pointer-events: none;
}
.topo .live-ip {
  fill: var(--fg3);
  font-family: var(--font-mono);
  font-size: 9px;
  text-anchor: middle;
  pointer-events: none;
}
/* Absolutely positioned over the .topo SVG's own box (see TopologyDiagram's
   inline position/inset rules) — no height/centering here on purpose. A
   fixed height plus center-alignment used to make this box taller than the
   space actually available above the hub, pushing its vertically-centered
   text down onto the hub itself; sizing now lives entirely in the inline
   style so the two can't drift apart again. */
.topo-empty {
  display: flex;
  justify-content: center;
  color: var(--fg3);
  font-size: var(--text-sm);
}

/* --- world-map topology view (ADR-0021, #11) ----------------------------- */
/* Shares .topo's hub-pulse/hub-core/node-ring/node-label rules (TopologyWorldMap
   renders <svg class="topo worldmap">) — only the ocean/land fills and the
   link styling below are new. No gradients, monochrome land mass, themed
   entirely through existing surface/border tokens. */
.worldmap-ocean {
  fill: var(--surface-2);
}
.worldmap-land {
  fill: var(--border);
  stroke: none;
}
/* stroke-width (and, for -down, stroke-dasharray) are set inline by
   TopologyWorldMap.js's linkStyle(), scaled by 1/zoom — a fixed value here
   would stretch into ever-thicker lines / ever-longer dashes while zooming
   in. LINK_STROKE_WIDTH/LINK_DASH/LINK_GAP there are this class's real
   source of truth; keep them in sync if these ever change. */
.worldmap-link {
  fill: none;
}
.worldmap-link-live {
  stroke: var(--status-ok, #16a34a);
  opacity: 0.55;
}
.worldmap-link-down {
  stroke: var(--fg3);
  opacity: 0.35;
}
/* Traffic-tier reinforcement (issue #34) — stroke-width itself is set
   inline by linkStyle() (see the comment above .worldmap-link), scaled by
   1/zoom; these two only lift opacity so a flowing/heavy link also reads
   brighter, not just thicker. */
.worldmap-link-flowing       { opacity: 0.8; }
.worldmap-link-flowing-heavy { opacity: 1; }
/* edge-hub-entity: the central-hub↔edge-hub relationship itself, not a
   site's reachability — stroke-width/dasharray fixed (not scaled by
   1/zoom like -live/-down) since this line is informational structure,
   not a signal whose exact thickness carries meaning. Deliberately
   neither -live's green nor -down's "known offline" grey: there's no
   handshake data behind it yet (see TopologyWorldMap.js's backboneLinks
   comment), so it can't honestly claim either state. */
.worldmap-link-backbone {
  stroke: var(--fg3);
  stroke-width: 1;
  stroke-dasharray: 2 3;
  opacity: 0.45;
}
/* One square per network under a multi-network gateway's pin (see GRID_*
   in TopologyWorldMap.js) — same accent as the topology diagram's network
   count text, not a new color. Filled (not just outlined) so it reads as a
   solid glyph at this size instead of a hollow square blending into the
   land/ocean fill behind it. */
.worldmap-net-square {
  fill: var(--accent);
  stroke: none;
}
.worldmap-net-count {
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 700;
  /* Sits on top of the now solid-accent-filled .worldmap-net-square (the
     "too many to enumerate" overflow block) — needs the on-accent contrast
     token, not --accent, or the number disappears into its own background. */
  fill: var(--fg-on-accent);
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}

/* --- connection activity heatmap (#32) ----------------------------------- */
/* 6 intensity levels off the existing accent ramp, GitHub-contribution-graph
   style. For the "hits" (connections) metric, level is an absolute duration
   bucket (0-3h/4-7h/8-11h/12-15h/16-19h/20-24h) so a peer connected 22h
   always reads as the brightest color, not just "more than today's other
   peers"; the "bytes" (traffic) metric still uses 5 levels relative to that
   day's max, since traffic volume has no natural absolute scale — see
   ActivityHeatmap.js's level(). */
/* `.table td { height: var(--row-height) }` (components.css) outranks a bare
   `.heatmap-cell` on specificity (class+element beats class alone), so every
   day cell rendered at the shared ~44px row height while staying 18px wide —
   not square. `td.heatmap-cell` matches that specificity to actually win.
   The sticky peer-name cell in the same row is given a matching explicit
   height inline (ActivityHeatmap.js) — a table row's rendered height is the
   max across all its cells, so the day cells alone being 20px tall isn't
   enough if the name cell in the same row still demands the default 44px. */
/* The avatar in a heatmap row identifies whose peer it is — it reports no
   state, and this card is read for the colour of the cells. At full
   saturation a column of faces competes with the data it is labelling, so
   they run desaturated here. Covers both halves of the avatar chain: a photo
   and the deterministic initials tile, because the filter sits on the root
   element rather than on the image. */
.hm-peer-label .avatar { filter: grayscale(1); }

td.heatmap-cell {
  width: 20px;
  height: 20px;
  border-radius: 3px;
  background: var(--surface-2);
}
/* Same specificity trap as the width/height rule above: `td.heatmap-cell`
   (element+class) outranks a bare `.heatmap-lN` (class only), so the level
   color never actually applied in light mode — every cell silently fell
   back to td.heatmap-cell's --surface-2, regardless of activity. It only
   looked correct in dark mode because those overrides added an extra
   [data-theme="dark"] attribute selector, which happened to out-rank it.
   `td.heatmap-cell.heatmap-lN` matches td.heatmap-cell's specificity plus
   the level class, so it wins in both themes without relying on that. */
td.heatmap-cell.heatmap-l1 { background: var(--accent-100); }
td.heatmap-cell.heatmap-l2 { background: var(--accent-200); }
td.heatmap-cell.heatmap-l3 { background: var(--accent-300); }
td.heatmap-cell.heatmap-l4 { background: var(--accent-400); }
td.heatmap-cell.heatmap-l5 { background: var(--accent-500); }
td.heatmap-cell.heatmap-l6 { background: var(--accent-600); }
[data-theme="dark"] td.heatmap-cell.heatmap-l1 { background: var(--accent-900); }
[data-theme="dark"] td.heatmap-cell.heatmap-l2 { background: var(--accent-800); }
[data-theme="dark"] td.heatmap-cell.heatmap-l3 { background: var(--accent-700); }
[data-theme="dark"] td.heatmap-cell.heatmap-l4 { background: var(--accent-600); }
[data-theme="dark"] td.heatmap-cell.heatmap-l5 { background: var(--accent-500); }
[data-theme="dark"] td.heatmap-cell.heatmap-l6 { background: var(--accent-400); }

/* A site gateway peer with zero activity that day is an outage worth
   flagging — unlike a client peer's normal, expected quiet day (laptop
   closed, phone off wifi), which stays the plain heatmap-l0 look above. */
td.heatmap-cell.heatmap-down { background: var(--danger-bg); border: 1px solid var(--danger-border); }

/* ============================================================================
   ResourcesView — card grid
   ============================================================================ */

.res-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: var(--space-4);
}

.res-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  transition: border-color 160ms cubic-bezier(0.2,0,0.2,1),
              box-shadow 160ms cubic-bezier(0.2,0,0.2,1);
}

.res-card:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
}

.res-card-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
}

.res-type-tile {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  background: var(--accent-soft-bg);
  border: 1px solid var(--accent-soft-border);
  color: var(--accent);
  display: grid;
  place-items: center;
  flex-shrink: 0;
}

.res-identity {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.res-name {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--fg1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.res-actions {
  display: flex;
  gap: var(--space-1);
  flex-shrink: 0;
  opacity: 0;
  transition: opacity 120ms cubic-bezier(0.2,0,0.2,1);
}

/* Fading in on hover is a nicety for a mouse and a dead end for a finger:
   without hover the button never appears, and editing a resource becomes
   impossible on a tablet. Where the pointer cannot hover, the action is
   simply always there. */
@media (hover: none) {
  .res-actions {
    opacity: 1;
  }
}

/* focus-within, not just hover: a button at opacity 0 still takes keyboard
   focus, so tabbing through the list used to move the focus ring onto
   something invisible. */
.res-card:hover .res-actions,
.res-card:focus-within .res-actions {
  opacity: 1;
}

.res-desc {
  padding: 0 var(--space-4) var(--space-3);
  font-size: var(--text-xs);
  color: var(--fg3);
  font-family: var(--font-sans);
  text-transform: none;
  letter-spacing: 0;
  line-height: 1.5;
}

.res-ports-section {
  border-top: 1px solid var(--border-subtle);
  padding: var(--space-3) var(--space-4);
  background: var(--surface-2);
}

.res-ports-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

/* Three buttons (add, from group, scan) plus the label no longer fit one row
   at the grid's own 360px card minimum — port-scan-range added the third
   one after this layout was set. Wrapping instead of clipping: the card's
   own overflow:hidden was cutting "Ports scannen" off mid-word rather than
   showing it on a line of its own. */
.res-ports-header > div {
  flex-wrap: wrap;
}

.res-port-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  min-height: 24px;
  align-items: center;
}

.res-port-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 24px;
  padding: 0 var(--space-2) 0 var(--space-3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
}

.res-port-remove {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--fg3);
  padding: 0 2px;
  font-size: 10px;
  line-height: 1;
  display: flex;
  align-items: center;
  border-radius: 2px;
  transition: color 120ms, background 120ms;
}

.res-port-remove:hover {
  color: var(--danger-fg);
  background: var(--danger-bg);
}

/* Port chip in edit mode — the inline form below belongs to this chip. */
.res-port-chip-editing {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}

/* Configured values that change what a port *is* (path prefix, capacity
   ceiling), readable straight off the chip — issue #82. */
.res-port-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 0 6px;
  height: 16px;
  border-radius: var(--radius-full);
  background: var(--surface2);
  color: var(--fg2);
  font-size: 10px;
  line-height: 1;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.res-port-edit {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--fg3);
  padding: 0 2px;
  font-size: 10px;
  line-height: 1;
  display: flex;
  align-items: center;
  border-radius: 2px;
  transition: color 120ms, background 120ms;
}

.res-port-edit:hover {
  color: var(--accent);
  background: var(--surface2);
}

.res-inline-form {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}

.res-form-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: flex-end;
}

/* Type picker grid in the modal */
.res-type-grid {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: var(--space-2);
}

.res-type-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: var(--text-xs);
  color: var(--fg2);
  background: var(--surface);
  transition: border-color 120ms, background 120ms, color 120ms;
  text-align: center;
  position: relative;
  -webkit-user-select: none;
  user-select: none;
}

.res-type-option:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
  color: var(--fg1);
}

.res-type-option.active {
  border-color: var(--accent);
  background: var(--accent-soft-bg);
  color: var(--accent);
}

/* ============================================================================
   Peer device type picker (in create/edit modal)
   ============================================================================ */

.peer-device-type-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-2);
}

.peer-device-type-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-2) var(--space-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: var(--text-xs);
  color: var(--fg2);
  background: var(--surface);
  transition: border-color 120ms, background 120ms, color 120ms;
  text-align: center;
  position: relative;
  -webkit-user-select: none;
  user-select: none;
}

.peer-device-type-option:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
  color: var(--fg1);
}

.peer-device-type-option.active {
  border-color: var(--accent);
  background: var(--accent-soft-bg);
  color: var(--accent);
}

/* ============================================================================
   AllResourcesView — grouped asset register
   ============================================================================ */

.allres-groups {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.allres-group {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.allres-group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-3) var(--space-4);
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}

.allres-rows {
  display: flex;
  flex-direction: column;
}

.allres-row {
  display: grid;
  grid-template-columns: 40px 1fr 140px 1fr;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  /* This is the one list that builds its rows itself instead of using .table,
     so it did not inherit the accent hairline every other list got. Same
     treatment, same reason: a row that highlights in grey is every tool ever
     built. Inset rather than a border, so the 40px icon column does not move. */
  box-shadow: inset 2px 0 0 0 transparent;
  transition:
    background var(--dur-fast) var(--ease-standard),
    box-shadow var(--dur-fast) var(--ease-standard);
}

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

.allres-row:hover {
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 0 var(--accent);
}

.allres-icon-col {
  display: flex;
  align-items: center;
  justify-content: center;
}

.allres-type-dot {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  background: var(--surface-3);
  border: 1px solid var(--border);
  color: var(--fg2);
  display: grid;
  place-items: center;
}

.allres-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.allres-name {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--fg1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.allres-type-label {
  font-size: var(--text-xs);
  color: var(--fg3);
  font-family: var(--font-sans);
  text-transform: none;
  letter-spacing: 0;
}

.allres-ip {
  font-size: var(--text-xs);
  color: var(--fg2);
  white-space: nowrap;
}

.allres-ports {
  font-size: var(--text-xs);
  color: var(--fg2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ============================================================================
   MyAccessView — freigegebene Ressourcen
   ============================================================================ */

.myaccess-grants {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
  gap: var(--space-4);
  align-items: start;
}

.myaccess-site-label {
  grid-column: 1 / -1;
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--fg3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: var(--space-4) 0 var(--space-2);
  border-top: 1px solid var(--border-subtle);
  margin-top: var(--space-2);
}

.myaccess-site-label:first-child {
  border-top: none;
  margin-top: 0;
  padding-top: 0;
}

.myaccess-resource {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.myaccess-resource-head {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.myaccess-ports {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-left: calc(32px + var(--space-3));
}

/* A reservation-gated port the user does not currently hold: the grant is
   real, so it stays visible, but it is not reachable until a slot is held —
   dimmed rather than hidden, so the user can still see what they would get.
   Applied per port, not to the whole row: a held RDP seat must not grey out
   the SSH chip next to it. The wrapper keeps pointer events so the title
   tooltip still explains why, while the link inside it is inert. */
/* Contact link to whoever holds the slot (#72) — quiet by default, since it
   sits inside an informational line, not an action row. */
.myaccess-holder-mail {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--text-xs); color: var(--fg2);
  text-decoration: none; border-bottom: 1px dotted var(--border-strong);
}
.myaccess-holder-mail:hover { color: var(--accent); border-bottom-color: var(--accent); }

.myaccess-port-slot { display: contents; }
.myaccess-port-slot--locked { display: inline-flex; opacity: 0.45; }
.myaccess-port-slot--locked a,
.myaccess-port-slot--locked button { pointer-events: none; }

/* Exclusive-capacity reservation strip (#72) — sits between the resource
   head and its ports, indented to the same gutter so it reads as belonging
   to that resource rather than as a page-level notice. */
.myaccess-reservation {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding-left: calc(32px + var(--space-3));
  margin-bottom: var(--space-2);
}

.myaccess-port-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 26px;
  padding: 0 var(--space-3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  color: var(--fg2);
}

.myaccess-port-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 26px;
  padding: 0 var(--space-3);
  background: var(--accent-soft-bg);
  border: 1px solid var(--accent-soft-border);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  color: var(--accent);
  text-decoration: none;
  transition: background 120ms, border-color 120ms;
}

.myaccess-port-link:hover {
  background: var(--accent-soft-border);
  border-color: var(--accent);
}

.myaccess-port-rdp {
  cursor: pointer;
  font-family: inherit;
  background: var(--accent-soft-bg);
  border-color: var(--accent-soft-border);
}

.myaccess-port-rdp:hover {
  background: var(--accent-soft-border);
  border-color: var(--accent);
}

.myaccess-port-rdp-group {
  display: inline-flex;
  gap: 2px;
}

.myaccess-port-rdp-left {
  border-top-right-radius: 5px;
  border-bottom-right-radius: 5px;
}

.myaccess-port-rdp-right {
  border-top-left-radius: 5px;
  border-bottom-left-radius: 5px;
}

/* browser button when adjacent to native split buttons */
.myaccess-port-rdp-browser-adj {
  border-top-left-radius: 5px;
  border-bottom-left-radius: 5px;
}

/* the "open in browser" button carries its own accent so it reads as the
   IronRDP action at a glance, not just another monitor icon in the row */
.myaccess-port-rdp-browser {
  color: var(--accent-strong, var(--accent));
  font-weight: 600;
}

/* IronRDP full-screen overlay */
.rdp-overlay {
  position: fixed;
  inset: 0;
  z-index: 900;
  background: #000;
  display: flex;
  flex-direction: column;
}

.rdp-toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  min-height: 40px;
}

.rdp-toolbar-title {
  font-weight: 600;
  font-size: var(--text-sm);
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rdp-close-btn {
  margin-left: auto;
  flex-shrink: 0;
}

.rdp-canvas {
  flex: 1;
  width: 100%;
  display: block;
  /* Baseline cursor. IronRDP delivers the remote pointer via the WASM
     setCursorStyleCallback (which sets an inline cursor on this element), not by
     compositing it into the framebuffer. `default` here means an invalid/empty
     callback value still leaves a visible cursor instead of hiding it. */
  cursor: default;
  outline: none;
}

.myaccess-port-vnc {
  --vnc-accent: #7C6AF7;
  cursor: pointer;
  background: color-mix(in srgb, var(--vnc-accent) 10%, transparent);
  border-color: color-mix(in srgb, var(--vnc-accent) 25%, transparent);
  color: var(--vnc-accent);
}

.myaccess-port-vnc:hover {
  background: color-mix(in srgb, var(--vnc-accent) 20%, transparent);
  border-color: var(--vnc-accent);
}

.myaccess-port-ssh,
.myaccess-port-sftp {
  --ssh-accent: #2EA87E;
  background: color-mix(in srgb, var(--ssh-accent) 10%, transparent);
  border-color: color-mix(in srgb, var(--ssh-accent) 25%, transparent);
  color: var(--ssh-accent);
}

.myaccess-port-ssh:hover,
.myaccess-port-sftp:hover {
  background: color-mix(in srgb, var(--ssh-accent) 20%, transparent);
  border-color: var(--ssh-accent);
}

.myaccess-port-smb {
  --smb-accent: #D07C2A;
  background: color-mix(in srgb, var(--smb-accent) 10%, transparent);
  border-color: color-mix(in srgb, var(--smb-accent) 25%, transparent);
  color: var(--smb-accent);
}

.myaccess-port-smb:hover {
  background: color-mix(in srgb, var(--smb-accent) 20%, transparent);
  border-color: var(--smb-accent);
}

/* "Mein Zugang" layout: order sections by usage frequency.
   Steady state (has a device): resources → devices → collapsed setup.
   Onboarding (no device): setup (open) → devices → resources. */
.myaccess-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}
.myaccess-resources { order: 1; }
.myaccess-devices   { order: 2; }
.myaccess-setup     { order: 3; }
/* The stack orders its children explicitly, so a new child without an `order`
   takes the default 0 and lands above all of them. The project note belongs
   under everything, not over it. */
.myaccess-project-note { order: 4; }
.myaccess-stack.is-onboarding .myaccess-setup     { order: 1; }
.myaccess-stack.is-onboarding .myaccess-devices   { order: 2; }
.myaccess-stack.is-onboarding .myaccess-resources { order: 3; }

/* Collapsible WireGuard-client install guide. */
.myaccess-setup {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
}
.myaccess-setup > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-md);
  font-weight: 600;
  -webkit-user-select: none;
  user-select: none;
  border-radius: var(--radius-md);
  transition: background 160ms cubic-bezier(0.2, 0, 0.2, 1);
}
.myaccess-setup > summary::-webkit-details-marker { display: none; }
.myaccess-setup > summary:hover { background: var(--surface-raised); }
.myaccess-setup[open] > summary {
  border-radius: var(--radius-md) var(--radius-md) 0 0;
}
.myaccess-setup-chevron {
  flex-shrink: 0;
  color: var(--text-muted);
  transition: transform 160ms cubic-bezier(0.2, 0, 0.2, 1);
}
.myaccess-setup[open] .myaccess-setup-chevron { transform: rotate(90deg); }
.myaccess-setup-detected {
  margin-left: auto;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}
.myaccess-setup-body {
  padding: var(--space-4);
  border-top: 1px solid var(--border-subtle);
  max-width: 720px;
}
.myaccess-legal {
  margin: var(--space-4) 0 0;
  font-size: var(--text-xs);
  line-height: 1.5;
  color: var(--fg3);
}

/* A sticky first column paints its own background over the row, so the accent
   hairline and the hover tint both disappeared behind it — visible everywhere
   except the one table where the left column is pinned, which is the ACL
   matrix. The hairline moves onto the cell there, and the cell answers hover
   like the rest of the row. */
.table tbody tr > td.sticky-col {
  /* The background belongs here rather than inline: an inline value beats any
     stylesheet rule, so the row's hover tint could never reach these cells. */
  background: var(--surface);
}

.table tbody tr:hover > td.sticky-col {
  background: var(--surface-2);
}

/* The hairline between the pinned pair and the scrolling columns. */
.table tbody tr > td.sticky-col-divider {
  box-shadow: 1px 0 0 var(--border);
}

.table tbody tr > td.sticky-col:first-child {
  box-shadow: inset 2px 0 0 0 transparent;
  transition: background var(--dur-fast) var(--ease-standard),
              box-shadow var(--dur-fast) var(--ease-standard);
}

.table tbody tr:hover > td.sticky-col:first-child {
  box-shadow: inset 2px 0 0 0 var(--accent);
}

/* Both shadows on one element, or the divider would cancel the hairline. */
.table tbody tr:hover > td.sticky-col-divider {
  box-shadow: 1px 0 0 var(--border);
}

/* Quiet by default: the note says what this is without competing with the
   devices the page is actually for. */
.myaccess-project-note {
  margin: var(--space-10) 0 var(--space-4);
  text-align: center;
  font-size: var(--text-xs);
  color: var(--fg3);
}

/* Always a link — it simply does not look like one until reached for. Making
   the link itself appear on hover would leave it unreachable on a phone, which
   the brief rules out; the same mistake was fixed on the resource cards and on
   the avatar badge. Colour and underline answer hover and keyboard focus
   alike, so the keyboard is not a second-class way through. */
.myaccess-project-link {
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: color var(--dur-fast) var(--ease-standard);
}

.myaccess-project-link:hover,
.myaccess-project-link:focus-visible {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.myaccess-project-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* No hover to reveal it with — show it as a link from the start. */
@media (hover: none) {
  .myaccess-project-link {
    color: var(--accent);
  }
}
@media (prefers-reduced-motion: reduce) {
  .myaccess-setup-chevron,
  .myaccess-setup > summary { transition: none; }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  @keyframes pulse { 0%, 100% { opacity: 1; } }
}

/* Someone who asked the system for less motion gets every state, just without
   the travel — they still see what is interactive. */
@media (prefers-reduced-motion: reduce) {
  .sidebar .nav-item,
  .sidebar .nav-item svg,
  .table tbody tr,
  .allres-row {
    transition: none;
  }
  .sidebar .nav-item:active,
  .card-hover:active {
    transform: none;
  }
}
