/* ==========================================================================
   shouter — operations console
   Built on the Cyberpunk HUD design system (see ../DESIGN.md).
   Tokens §2, effects layer §6, components §7. Zero external dependencies.
   Overrides belong in theme.css, which loads after this file.
   ========================================================================== */

@font-face {
  font-family: "JetBrains Mono";
  src: url("fonts/JetBrainsMono-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("fonts/JetBrainsMono-Bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* ------------------------------- §2 tokens ------------------------------- */
:root {
  --accent-rgb:     0 255 245;
  --accent-alt-rgb: 255 43 214;
  --warn-rgb:       255 183 0;
  --ok-rgb:         57 255 136;
  --alert-rgb:      255 59 92;

  --accent:     rgb(var(--accent-rgb));
  --accent-alt: rgb(var(--accent-alt-rgb));
  --warn:       rgb(var(--warn-rgb));
  --ok:         rgb(var(--ok-rgb));
  --alert:      rgb(var(--alert-rgb));

  --bg-void:        #05070c;
  --bg-panel:       rgb(10 18 28 / 0.55);
  --bg-panel-solid: #0a1220;
  --bg-raised:      rgb(var(--accent-rgb) / 0.04);
  --bg-sunken:      rgb(0 0 0 / 0.35);

  --text-primary: #d9fbff;
  --text-dim:     #6f97a3;

  --line-faint:  rgb(var(--accent-rgb) / 0.08);
  --line-soft:   rgb(var(--accent-rgb) / 0.14);
  --line-mid:    rgb(var(--accent-rgb) / 0.25);
  --line:        rgb(var(--accent-rgb) / 0.35);
  --line-strong: rgb(var(--accent-rgb) / 0.60);

  --s-1: 4px;  --s-2: 8px;  --s-3: 12px; --s-4: 16px;
  --s-5: 24px; --s-6: 32px; --s-7: 48px;

  /* Shell geometry, shared verbatim with the sibling consoles — neetsec,
     neetmap, hubergine, noderunner and phantop all hard-code these same
     numbers as `.hud` and `.hud-header` in the vendored styles.css. shouter
     had been using the nearest --s-* rungs instead (12px / 12px 24px / 16px),
     which reads visibly tighter than the others when two of them are open
     side by side, and made the header sit a few pixels shallower.
     They are tokens rather than literals because they are deliberately OFF
     the --s-* ladder: 14, 18, 26 and 20 are not spacing-scale values, and a
     later "tidy-up" onto the ladder would silently break the match again.
     The header is asymmetric on purpose — the wordmark on the left is
     optically lighter than the controls on the right. */
  --shell-pad-y:  14px;
  --shell-pad-x:  18px;
  --shell-gap:    14px;
  --header-pad-y: 12px;
  --header-pad-l: 18px;
  --header-pad-r: 26px;
  --header-gap:   20px;

  --r-sm: 2px; --r-md: 3px; --r-lg: 6px;
  --cut: 16px;

  --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono",
    "Share Tech Mono", Consolas, "SFMono-Regular", "Courier New", monospace;
  --fs-micro: 0.6rem;
  --fs-label: 0.7rem;
  --fs-body:  0.82rem;
  --fs-lead:  1rem;
  --fs-h3:    1.15rem;
  --fs-h2:    1.6rem;
  --fs-h1:    2.2rem;
  --track-label: 0.15em;
  --track-title: 0.18em;

  --scanline-opacity: 0.18;
  --noise-opacity:    0.035;
  --glow-sm: 0 0 6px;
  --glow-md: 0 0 12px;
  --glow-lg: 0 0 22px;

  --t-fast: 0.15s; --t-med: 0.3s; --t-slow: 0.6s;
  --ease: cubic-bezier(0.4, 0, 0.2, 1);
}

/* -------------------------------- reset ---------------------------------- */
* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
}

body {
  background:
    radial-gradient(ellipse at 20% 0%,   rgb(var(--accent-rgb) / 0.07), transparent 55%),
    radial-gradient(ellipse at 85% 100%, rgb(var(--accent-alt-rgb) / 0.06), transparent 55%),
    var(--bg-void);
  background-attachment: fixed;
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: var(--fs-body);
  line-height: 1.5;
  min-height: 100vh;
}

[hidden] { display: none !important; }

* { scrollbar-width: thin; scrollbar-color: rgb(var(--accent-rgb) / 0.4) transparent; }
*::-webkit-scrollbar { width: 6px; height: 6px; }
*::-webkit-scrollbar-thumb {
  background: rgb(var(--accent-rgb) / 0.35);
  border-radius: var(--r-md);
}

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
::selection { background: var(--accent); color: #000; }

/* ---------------------------- §6 effects layer --------------------------- */
.scanlines, .vignette, .noise {
  position: fixed;
  inset: 0;
  pointer-events: none;
}
.scanlines {
  z-index: 100;
  background: repeating-linear-gradient(to bottom,
    rgb(0 0 0 / 0) 0px, rgb(0 0 0 / 0) 1px,
    rgb(0 0 0 / var(--scanline-opacity)) 2px, rgb(0 0 0 / 0) 3px);
  mix-blend-mode: overlay;
  animation: scan-drift 9s linear infinite;
}
@keyframes scan-drift {
  from { background-position-y: 0; }
  to   { background-position-y: 100px; }
}
.vignette { z-index: 99; box-shadow: inset 0 0 220px rgb(0 0 0 / 0.85); }
.noise {
  z-index: 98;
  opacity: var(--noise-opacity);
  background-image: radial-gradient(circle, #fff 1px, transparent 1px);
  background-size: 3px 3px;
}

/* ------------------------------- §5 frame -------------------------------- */
.frame {
  position: relative;
  background: var(--bg-panel);
  border: 1px solid var(--line);
  backdrop-filter: blur(10px) saturate(140%);
  -webkit-backdrop-filter: blur(10px) saturate(140%);
  clip-path: polygon(
    var(--cut) 0, 100% 0,
    100% calc(100% - var(--cut)), calc(100% - var(--cut)) 100%,
    0 100%, 0 var(--cut)
  );
  box-shadow:
    inset 0 0 0 1px rgb(var(--accent-rgb) / 0.05),
    0 0 18px rgb(var(--accent-rgb) / 0.06),
    0 4px 24px rgb(0 0 0 / 0.4);
}
.frame::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  clip-path: inherit;
  border: 1px solid transparent;
  background: linear-gradient(135deg,
      rgb(var(--accent-rgb) / 0.5), transparent 30%,
      transparent 70%, rgb(var(--accent-alt-rgb) / 0.35)) border-box;
  -webkit-mask: linear-gradient(#fff 0 0) padding-box,
                linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}

/* ----------------------------- §3 typography ----------------------------- */
.label {
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-dim);
}
.title {
  font-size: var(--fs-h3);
  letter-spacing: var(--track-title);
  text-transform: uppercase;
  color: var(--accent);
  text-shadow: var(--glow-sm) rgb(var(--accent-rgb) / 0.5);
}

/* ------------------------------ §7 buttons ------------------------------- */
.btn {
  font-family: inherit;
  font-size: var(--fs-label);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  background: rgb(var(--accent-rgb) / 0.08);
  border: 1px solid rgb(var(--accent-rgb) / 0.45);
  border-radius: var(--r-md);
  padding: var(--s-2) var(--s-4);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
}
.btn:hover:not(:disabled) {
  background: rgb(var(--accent-rgb) / 0.16);
  box-shadow: var(--glow-md) rgb(var(--accent-rgb) / 0.35);
}
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; }

.btn-solid {
  background: var(--accent);
  color: var(--bg-void);
  font-weight: 700;
}
.btn-solid:hover:not(:disabled) {
  background: var(--accent);
  box-shadow: var(--glow-lg) rgb(var(--accent-rgb) / 0.5);
}
.btn-ghost {
  background: transparent;
  border-color: var(--line-mid);
  color: var(--text-dim);
}
.btn-ghost:hover:not(:disabled) {
  color: var(--text-primary);
  background: rgb(var(--accent-rgb) / 0.08);
  box-shadow: none;
}
.btn-danger {
  color: var(--alert);
  border-color: rgb(var(--alert-rgb) / 0.45);
  background: rgb(var(--alert-rgb) / 0.08);
}
.btn-danger:hover:not(:disabled) {
  background: rgb(var(--alert-rgb) / 0.16);
  box-shadow: var(--glow-md) rgb(var(--alert-rgb) / 0.35);
}
.btn-sm { padding: var(--s-1) var(--s-3); font-size: var(--fs-micro); }

/* ------------------------------- §7 inputs ------------------------------- */
.input {
  width: 100%;
  font-family: inherit;
  font-size: var(--fs-body);
  color: var(--text-primary);
  background: var(--bg-sunken);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-md);
  padding: var(--s-2) var(--s-3);
  caret-color: var(--accent);
  transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.input::placeholder { color: var(--text-dim); }
.input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 1px rgb(var(--accent-rgb) / 0.4),
              var(--glow-md) rgb(var(--accent-rgb) / 0.25);
}
.input[aria-invalid="true"] { border-color: var(--alert); }

.field { display: flex; flex-direction: column; gap: var(--s-1); }
.field > label {
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-dim);
}
.field-hint {
  font-size: var(--fs-micro);
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

/* ------------------------------- §7 badges ------------------------------- */
.badge {
  display: inline-block;
  font-size: var(--fs-micro);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  padding: 1px var(--s-1);
  border: 1px solid currentColor;
  border-radius: var(--r-sm);
  color: var(--text-dim);
  white-space: nowrap;
}
.badge-ok    { color: var(--ok); }
.badge-warn  { color: var(--warn); }
.badge-alert { color: var(--alert); }
.badge-info  { color: var(--accent); }

.dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-dim);
  flex-shrink: 0;
}
.dot-ok    { background: var(--ok);    box-shadow: var(--glow-sm) var(--ok); }
.dot-warn  { background: var(--warn);  box-shadow: var(--glow-sm) var(--warn); }
.dot-alert { background: var(--alert); box-shadow: var(--glow-sm) var(--alert); }
.dot-live  { animation: pulse 2s var(--ease) infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }

/* ------------------------------- §7 panel -------------------------------- */
.panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  padding: var(--s-3) var(--s-4) var(--s-4);
  overflow: hidden;
}
.panel-title {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: var(--fs-h3);
  letter-spacing: var(--track-title);
  text-transform: uppercase;
  color: var(--accent);
  text-shadow: var(--glow-sm) rgb(var(--accent-rgb) / 0.5);
  border-bottom: 1px solid rgb(var(--accent-rgb) / 0.18);
  padding-bottom: var(--s-2);
  margin-bottom: var(--s-3);
  flex-shrink: 0;
}
.panel-meta {
  margin-left: auto;
  color: var(--text-dim);
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-shadow: none;
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
.panel-body { flex: 1; min-height: 0; }
.panel-body-col { display: flex; flex-direction: column; }

/* --------------------------- §7 empty / loading -------------------------- */
.state {
  padding: var(--s-6) var(--s-4);
  text-align: center;
  color: var(--text-dim);
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.state-alert { color: var(--alert); }

/* ==========================================================================
   Application layout
   ========================================================================== */

/* Boot splash, shown while the session check is in flight. */
.boot {
  min-height: 100vh;
  display: grid;
  place-items: center;
}

/* -------------------------------- login ---------------------------------- */
.login-view {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--s-5);
}
.login-card {
  width: min(420px, 100%);
  padding: var(--s-6) var(--s-5) var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}
.brand {
  margin: 0;
  font-size: var(--fs-h1);
  font-weight: 700;
  letter-spacing: var(--track-title);
  color: var(--accent);
  text-shadow: var(--glow-sm) rgb(var(--accent-rgb) / 0.7),
               var(--glow-lg) rgb(var(--accent-rgb) / 0.35);
}
.brand-sub {
  margin: var(--s-1) 0 0;
  font-size: var(--fs-label);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-dim);
}
.login-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-4);
}
.login-form {
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
}

/* ---------------------------- language switch ---------------------------- */
/* Segmented, in the shape of the §7 tabs: the current locale is the accent
   one, so the control reads as state rather than as two buttons. */
.lang-switch {
  display: flex;
  flex-shrink: 0;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-md);
  overflow: hidden;
}
.lang-btn {
  font-family: inherit;
  font-size: var(--fs-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: transparent;
  border: 0;
  padding: var(--s-1) var(--s-2);
  cursor: pointer;
  transition: color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}
.lang-btn + .lang-btn { border-left: 1px solid var(--line-faint); }
.lang-btn:hover { color: var(--text-primary); }
.lang-btn[aria-pressed="true"] {
  color: var(--accent);
  background: rgb(var(--accent-rgb) / 0.12);
  text-shadow: var(--glow-sm) rgb(var(--accent-rgb) / 0.5);
}
.login-foot {
  margin: 0;
  font-size: var(--fs-micro);
  letter-spacing: 0.06em;
  color: var(--text-dim);
  border-top: 1px solid var(--line-faint);
  padding-top: var(--s-3);
}
.form-error {
  margin: 0;
  font-size: var(--fs-label);
  letter-spacing: 0.06em;
  color: var(--alert);
  border-left: 2px solid var(--alert);
  padding-left: var(--s-2);
}

/* --------------------------------- shell --------------------------------- */
/* A definite height, not a minimum. With min-height the whole chain below is
   content-sized, so a long console pushes the page taller and the panels'
   own overflow rules never engage — the app scrolls instead of the output.
   Pinning the shell to the viewport is what makes the inner scrollers work. */
.app {
  height: 100vh;
  height: 100dvh;   /* mobile browsers, where 100vh includes the URL bar */
  overflow: hidden;
  padding: var(--shell-pad-y) var(--shell-pad-x);
  display: flex;
  flex-direction: column;
  gap: var(--shell-gap);
}
.app-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--header-gap);
  padding: var(--header-pad-y) var(--header-pad-r)
           var(--header-pad-y) var(--header-pad-l);
}
/* 1.6rem at 0.12em, matching `.alias` in the siblings. The 0.18em of
   --track-title is right for a panel title but too wide for a wordmark at
   this size — it was the other half of why the header did not line up. */
.app-header .brand {
  font-size: var(--fs-h2);
  letter-spacing: 0.12em;
}
/* 2px, as `.hostname` is in the siblings. --s-1 (4px) pushed the header two
   pixels deeper than theirs. Scoped to the header so the login card, which
   has no counterpart over there, keeps its roomier spacing. */
.app-header .brand-sub { margin-top: 2px; }
/* A grid with named tracks, not a flex row (§8 "never wrap a row of peers").
   The two `auto` tracks hold their content at every width, so a button never
   squashes; the operator gets the one flexible track and is the only thing
   that gives, which is what stops the login running into "Change password". */
.header-right {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: center;
  gap: var(--s-4);
}
.operator {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  min-width: 0;
}
.operator-label {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-dim);
}
.operator-login {
  font-size: var(--fs-body);
  color: var(--text-primary);
  /* 32ch so a long login cannot dominate a wide header; 100% so it still
     ellipsises when the track is narrower than that, rather than overflowing
     its own column. Both bounds are needed — either alone leaks one way. */
  max-width: min(32ch, 100%);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.header-actions { display: flex; gap: var(--s-2); }

/* Two shares that cannot starve each other (§8): the console never collapses
   because the operations list got long, and vice versa. */
.content {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr);
  /* An implicit row is `auto` — it sizes to its content and happily overflows
     the grid. minmax(0, 1fr) pins it to the container instead (DESIGN.md §8). */
  grid-template-rows: minmax(0, 1fr);
  /* The same 14px the shell uses between header and content, so the gutter
     between the two panels and the gutter above them are one measurement —
     which is what `.grid` does in the siblings. */
  gap: var(--shell-gap);
}

/* ------------------------------ operations ------------------------------- */
/* flex + min-height rather than height:100%, matching the console: a
   percentage height needs a definite parent, and this survives the panel
   gaining or losing a header row. */
.ops-list {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding-right: var(--s-1);
}
.op-group {
  font-size: var(--fs-micro);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-dim);
  padding: var(--s-2) 0 var(--s-1);
  border-bottom: 1px solid var(--line-faint);
}
.op-group:not(:first-child) { margin-top: var(--s-3); }

/* The whole card is the console's channel selector. Dragging across one to
   read it would otherwise start a text selection and swallow the click, so the
   card is unselectable — the console output, which people copy, is not. */
.op {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3);
  background: var(--bg-raised);
  border: 1px solid rgb(var(--accent-rgb) / 0.1);
  border-radius: var(--r-md);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: border-color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease);
}
.op:hover { border-color: var(--line-mid); background: rgb(var(--accent-rgb) / 0.07); }
.op.is-running { border-color: rgb(var(--warn-rgb) / 0.45); }

/* Which operation the console is currently showing. Loud enough to find at a
   glance with four of them running, quiet enough not to compete with the
   running border above. */
.op.is-selected {
  border-color: var(--line-strong);
  background: rgb(var(--accent-rgb) / 0.1);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* A real <button>, so selecting an operation works from the keyboard without
   inventing div-with-role semantics (DESIGN.md §10). Chrome reset to nothing,
   since the card around it carries the styling. */
.op-main {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
  font: inherit;
  color: inherit;
  text-align: left;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  cursor: inherit;
}
.op-name {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  font-weight: 700;
  color: var(--text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.op-desc {
  font-size: var(--fs-label);
  color: var(--text-dim);
  line-height: 1.45;
  /* Long descriptions wrap but never push the button column out (§8). */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.op-actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}
/* A status flipping idle → running must not shift the Execute button, so every
   badge reserves room for the longest label. That length is language-specific
   ("idle"/"running" vs "ожидание"/"выполняется"), so app.js measures the
   current locale's labels and sets --op-status-ch; the fallback covers the
   first paint before it does.

   min-width, not width: if the estimate is ever short the badge grows instead
   of clipping its own text. Both labels still resolve to the same number, so
   the column stays aligned either way. */
.op-status {
  min-width: calc(
    var(--op-status-ch, 9) * 1ch +      /* glyphs                   */
    var(--op-status-ch, 9) * 0.06em +   /* .badge letter-spacing    */
    var(--s-2) + 2px                    /* padding + 1px borders    */
  );
  text-align: center;
}

/* -------------------------------- console -------------------------------- */
.console-head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s-3);
  padding-bottom: var(--s-2);
  margin-bottom: var(--s-2);
  border-bottom: 1px solid var(--line-faint);
  flex-shrink: 0;
}
.console-id {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
}
.console-name {
  color: var(--text-primary);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.console-stats {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: var(--fs-label);
  color: var(--text-dim);
  letter-spacing: 0.06em;
}
.console-stats b {
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.console {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  background: var(--bg-sunken);
  border: 1px solid var(--line-faint);
  border-radius: var(--r-md);
  padding: var(--s-2);
  font-size: var(--fs-body);
  line-height: 1.45;
}
.cline {
  display: grid;
  grid-template-columns: 9ch minmax(0, 1fr);
  gap: var(--s-2);
  padding: 1px var(--s-1);
}
.cline-time {
  color: rgb(var(--accent-rgb) / 0.4);
  font-variant-numeric: tabular-nums;
  font-size: var(--fs-micro);
  padding-top: 2px;
}
.cline-text {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--text-primary);
}
.cline-stderr .cline-text { color: var(--alert); }
.cline-system .cline-text {
  color: var(--text-dim);
  font-style: normal;
  letter-spacing: 0.04em;
}
.console-cursor {
  display: inline-block;
  width: 0.6em;
  height: 1em;
  vertical-align: text-bottom;
  background: var(--accent);
  box-shadow: var(--glow-sm) rgb(var(--accent-rgb) / 0.6);
  animation: blink 1.1s steps(1) infinite;
}
@keyframes blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }

.console-foot {
  flex-shrink: 0;
  padding-top: var(--s-2);
  display: flex;
  align-items: center;
  gap: var(--s-3);
  font-size: var(--fs-micro);
  letter-spacing: 0.06em;
  color: var(--text-dim);
}
.console-foot .spacer { margin-left: auto; }
.console-foot label {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  cursor: pointer;
}
input[type="checkbox"] {
  accent-color: var(--accent);
  cursor: pointer;
  margin: 0;
}

/* --------------------------------- modal --------------------------------- */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgb(0 0 0 / 0.7);
  backdrop-filter: blur(3px);
  display: grid;
  place-items: center;
  padding: var(--s-4);
}
.modal {
  width: min(480px, 100%);
  padding: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  max-height: calc(100vh - var(--s-6));
  overflow-y: auto;
}
.modal-title {
  margin: 0;
  font-size: var(--fs-h3);
  letter-spacing: var(--track-title);
  text-transform: uppercase;
  color: var(--accent);
  text-shadow: var(--glow-sm) rgb(var(--accent-rgb) / 0.5);
}
.modal-text {
  margin: 0;
  color: var(--text-dim);
  font-size: var(--fs-label);
  line-height: 1.6;
}
.modal-text strong { color: var(--text-primary); }
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--s-2);
}

/* --------------------------------- toasts -------------------------------- */
.toasts {
  position: fixed;
  z-index: 300;
  bottom: var(--s-5);
  right: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  max-width: min(420px, calc(100vw - var(--s-6)));
}
.toast {
  padding: var(--s-3) var(--s-4);
  background: var(--bg-panel-solid);
  border-left: 3px solid var(--accent);
  border-radius: var(--r-md);
  box-shadow: 0 4px 24px rgb(0 0 0 / 0.5);
  font-size: var(--fs-body);
  animation: toast-in var(--t-med) var(--ease);
}
.toast-ok    { border-left-color: var(--ok); }
.toast-alert { border-left-color: var(--alert); }
.toast-warn  { border-left-color: var(--warn); }
@keyframes toast-in {
  from { opacity: 0; transform: translateX(12px); }
  to   { opacity: 1; transform: none; }
}

/* ------------------------------- responsive ------------------------------ */
/* Stacked on one column, two viewport-locked panels would each get half a
   screen and both feel cramped. Below this width the page scrolls normally
   and the panels size to their content instead. */
@media (max-width: 1020px) {
  .app {
    height: auto;
    min-height: 100vh;
    overflow: visible;
  }
  .content {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto;
  }
  /* A floor each, or the console arrives as a sliver. */
  .panel-ops     { min-height: 240px; }
  .panel-console { min-height: 340px; }
  /* Content-sized here, so an inner scroller has nothing to scroll against;
     cap them instead and let each keep its own overflow. */
  .ops-list { max-height: 60vh; }
  .console  { max-height: 60vh; }
}

@media (max-width: 620px) {
  /* Stacked, gap 10px — same as the siblings' 720px block. The breakpoint
     stays 620px here, not 720px: theirs is set by a four-column spec row that
     shouter's header does not have, and moving it would stack the operator
     block a whole size class earlier than it needs to. */
  .app-header {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }
  /* One column. Three peers on a phone row cannot all keep their labels, and
     wrapping them would staircase (§8) — so each gets its own full-width row
     and nothing has to shrink at all. */
  .header-right {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-3);
  }
  /* Identity leads the column instead of being stranded below the buttons.
     Reordering visually is safe here specifically because .operator contains
     no focusable element, so tab order still matches what is on screen. */
  .operator {
    order: -1;
    align-items: flex-start;
  }
  /* A grid item stretches to its track by default, which turned the two-button
     EN/RU control into a full-width bar with the labels stranded at one end.
     It is a segmented control, not a row of actions — size it to its content.
     `flex-shrink: 0` on the base rule does not cover this: that governs
     shrinking, and this was the item being stretched. */
  .lang-switch { justify-self: start; }
  /* The buttons do want the full width: comfortable tap targets, and it lets
     "Изменить пароль" stay on one line at 320px. */
  .header-actions > .btn { flex: 1; }
  .op { grid-template-columns: minmax(0, 1fr); }
  .op-actions { justify-content: flex-end; }
  .cline { grid-template-columns: minmax(0, 1fr); }
  .cline-time { display: none; }
}

/* ------------------------------- §9 motion ------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
