/* Operational details: identifiers, charts and irreversible controls remain explicit. */
.chart {
  width: 100%;
  min-width: 0
}

.chart svg {
  display: block;
  width: 100%;
  height: 110px
}

.bar-track {
  fill: var(--c-surface-2)
}

.bar {
  fill: var(--c-teal)
}

.chart-legend {
  display: flex;
  justify-content: space-between;
  margin-top: .35rem;
  color: var(--c-muted);
  font: .68rem var(--mono)
}

.tile-spark {
  display: block;
  margin-top: .5rem
}

.tile-spark svg {
  display: block;
  width: 100%;
  height: 26px
}

.tile-spark .bar {
  fill: var(--c-teal)
}

.panel-tools {
  display: flex;
  align-items: center;
  gap: .4rem;
  flex-wrap: wrap
}

/* A range picker inside a panel-head should read as a small control, not
   a full-width form field. */
.chart-range {
  display: flex;
  align-items: center;
  gap: .4rem
}

.chart-range select {
  width: auto;
  padding: .3rem .5rem;
  font-size: .8rem
}

.panel-note {
  margin: .75rem 0 0;
  color: var(--c-muted);
  font-size: .82rem
}

.danger-zone {
  border-color: var(--c-bad);
  border-top-width: 3px
}

.danger-zone .panel-head {
  background: var(--c-bad-dim);
  border-bottom-color: color-mix(in srgb, var(--c-bad) 45%, transparent)
}

.warn-list {
  margin: .5rem 0 1rem;
  padding-left: 1.15rem
}

.warn-list li {
  margin: .35rem 0
}

.check {
  display: flex;
  align-items: flex-start;
  gap: .6rem;
  margin: .65rem 0;
  cursor: pointer
}

.check input {
  margin-top: .25rem;
  accent-color: var(--c-teal)
}

.secret {
  display: block;
  min-width: 0
}

.secret code,
.mono,
code {
  font-family: var(--mono);
  overflow-wrap: anywhere;
  word-break: break-word
}

.dtable .mono,
.dtable td {
  overflow-wrap: anywhere
}

.code-block {
  margin: .5rem 0;
  padding: .7rem .85rem;
  border: 1px solid var(--c-border);
  border-radius: .4rem;
  background: var(--c-surface-2);
  overflow-x: auto;
  white-space: pre;
  font-size: .82rem
}

/* columns that must never break mid-token (phone numbers, status badges) */
.dtable td.nowrap {
  white-space: nowrap;
  overflow-wrap: normal
}

.dtable .sms-status {
  white-space: nowrap
}

/* let the message column soak up the wrapping instead */
.sms-log-body {
  min-width: 12rem
}

.btn-danger:hover {
  background: var(--c-bad);
  color: var(--warm-ivory)
}

.btn-icon {
  width: 32px;
  height: 32px;
  padding: 0;
  flex: none
}

/* a bare <svg> as a direct flex child of .btn collapses in some engines;
   stop it shrinking and pin the size */
.btn .ci {
  flex: none
}

.btn-icon .ci {
  width: 18px;
  height: 18px
}

/* an icon button sitting next to a .btn-sm text button (contacts' Edit +
   Remove) -- .btn-icon's fixed 32px square is for a same-size icon-button
   row (the API tokens panel) and doesn't match .btn-sm's auto height, so
   here it borrows .btn-sm's own vertical padding instead of a fixed size */
.btn-icon.btn-sm {
  width: auto;
  height: auto;
  padding: .35rem
}

.btn-icon.btn-sm .ci {
  width: 16px;
  height: 16px
}

/* transient "Copied" / "Ctrl+C" label for the copy buttons (console.js
   sets data-flash); a floating bubble so an icon button keeps its icon */
.btn[data-flash] {
  position: relative
}

.btn[data-flash]::after {
  content: attr(data-flash);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  padding: .25rem .45rem;
  background: var(--c-text);
  color: var(--c-surface);
  font: 400 .66rem var(--sub);
  letter-spacing: .04em;
  white-space: nowrap;
  pointer-events: none;
  z-index: 5
}

.num {
  text-align: right;
  font-variant-numeric: tabular-nums
}

.when {
  white-space: nowrap;
  color: var(--c-muted);
  font-variant-numeric: tabular-nums
}

.stat-row {
  display: flex;
  gap: 1.5rem;
  flex-wrap: wrap
}

.stat-row>div {
  display: grid
}

.stat-row b {
  font: 400 1.7rem var(--display)
}

.stat-row span {
  color: var(--c-muted);
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .07em
}

.switch {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  cursor: pointer
}

.switch input {
  position: absolute;
  opacity: 0
}

.switch .track {
  position: relative;
  width: 38px;
  height: 21px;
  background: var(--c-surface-2);
  border: 1px solid var(--c-border)
}

.switch .track:after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 13px;
  height: 13px;
  background: var(--c-muted);
  transition: transform .15s
}

.switch input:checked+.track {
  background: var(--c-ok-dim);
  border-color: var(--c-ok)
}

.switch input:checked+.track:after {
  transform: translateX(16px);
  background: var(--c-ok)
}

@font-face {
  font-family: Sora;
  src: url("/fonts/sora.woff2") format("woff2");
  font-weight: 100 800;
  font-display: swap
}

@font-face {
  font-family: Manrope;
  src: url("/fonts/manrope.woff2") format("woff2");
  font-weight: 200 800;
  font-display: swap
}

:root {
  /* Innotech palette (brand guide p. 24-25), same values as style.css. */
  --deep-teal: #182F36;
  --warm-ivory: #F2F0E8;
  --vibrant-orange: #FB8C20;
  --vibrant-yellow: #F7E44F;
  --cool-silver: #E6E7E8;
  --soft-sage: #AFC7B8;
  /* Roles. Tints mix two palette colours (the guide gives no tint values);
     each text pairing meets WCAG AA. */
  --c-bg: var(--cool-silver);
  --c-rail: var(--deep-teal);
  --c-surface: var(--warm-ivory);
  --c-surface-2: color-mix(in srgb, var(--deep-teal) 7%, var(--warm-ivory));
  --c-border: color-mix(in srgb, var(--deep-teal) 24%, transparent);
  --c-border-soft: color-mix(in srgb, var(--deep-teal) 13%, transparent);
  --c-text: var(--deep-teal);
  --c-muted: color-mix(in srgb, var(--deep-teal) 75%, var(--warm-ivory)); /* 5.9:1 ivory, 5.4:1 silver */
  --c-faint: color-mix(in srgb, var(--deep-teal) 70%, var(--warm-ivory)); /* 5.0:1 ivory, 4.6:1 silver */
  --c-teal: var(--deep-teal);
  --c-teal-dim: var(--soft-sage);
  --c-orange: var(--vibrant-orange);
  --c-orange-dim: color-mix(in srgb, var(--vibrant-orange) 22%, var(--warm-ivory));
  --c-on-dark: var(--warm-ivory);
  --c-on-dark-muted: color-mix(in srgb, var(--warm-ivory) 75%, var(--deep-teal)); /* 7.6:1 on teal */
  --c-rail-hover: color-mix(in srgb, var(--warm-ivory) 10%, var(--deep-teal));
  --c-line-on-dark: color-mix(in srgb, var(--warm-ivory) 22%, transparent);
  /* Functional status colours: not brand colours (the guide has none). */
  --c-ok: #1d6b43;
  --c-ok-dim: #d9eadf;
  --c-warn: #8a5a00;
  --c-warn-dim: #f3e6c7;
  --c-bad: #a3231b;
  --c-bad-dim: #f3d9d4;
  /* Typography (brand guide p. 27-29) */
  --sans: Manrope, system-ui, sans-serif;     /* body: Manrope Light */
  --sub: Manrope, system-ui, sans-serif;      /* labels and subheadings: Manrope Regular */
  --display: Sora, system-ui, sans-serif;     /* main headings: Sora Regular */
  --mono: ui-monospace, monospace;
  --rail-w: 250px;
  --rail-w-collapsed: 64px
}

* {
  box-sizing: border-box
}

html,
body {
  margin: 0;
  background: var(--c-bg)
}

body.console {
  color: var(--c-text);
  font: 300 15px/1.55 var(--sans)
}

a {
  color: var(--c-teal)
}

h1,
h2 {
  font-family: var(--display);
  font-weight: 400;
  letter-spacing: -.02em
}

h3,
h4 {
  font-family: var(--sub);
  font-weight: 400
}

strong,
b {
  font-weight: 600
}

.ci {
  width: 1.1em;
  height: 1.1em;
  vertical-align: -.15em
}

.shell {
  display: grid;
  grid-template-columns: var(--rail-w) 1fr;
  grid-template-rows: 62px 1fr;
  grid-template-areas: "brand topbar" "rail main";
  min-height: 100dvh;
  transition: grid-template-columns .15s
}

.shell.rail-collapsed {
  grid-template-columns: var(--rail-w-collapsed) 1fr
}

/* Pin the whole top strip (brand + top bar) so the "// section" context and
   the account chip -- picture, email, menu -- stay in view while the page
   scrolls. The rail and main content scroll under it. */
.shell-brand,
.shell-topbar {
  position: sticky;
  top: 0;
  z-index: 5
}

.shell-brand {
  grid-area: brand;
  background: var(--c-rail);
  padding: 0 1rem 0 1.2rem;
  display: flex;
  align-items: center;
  gap: .65rem;
  color: var(--c-on-dark)
}

/* Full logo for dark grounds, 26px tall. Clear space X (width of the
   wordmark's "n") is 0.32 x height = 8px, kept by the .65rem gap. When
   the rail collapses to icons the symbol takes its place. */
.shell-brand .brand-full {
  display: block;
  flex: 0 0 auto;
  height: 26px;
  width: auto
}

.shell-brand .brand-symbol {
  display: none
}

.brand-wordmark {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 400 .88rem var(--sub)
}

.shell-topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0 2rem;
  background: var(--c-surface);
  border-bottom: 1px solid var(--c-border)
}

.shell-context {
  font: 400 .9rem var(--sub)
}

.shell-context:before {
  content: "// ";
  color: var(--c-muted)
}

.shell-spacer {
  flex: 1
}

.shell-user {
  position: relative
}

.shell-user summary {
  display: flex;
  align-items: center;
  gap: .5rem;
  cursor: pointer;
  list-style: none;
  color: var(--c-muted);
  font-size: .84rem
}

.avatar {
  flex: none;
  width: 27px;
  height: 27px;
  border-radius: 50%;
  object-fit: cover;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--c-surface-2);
  font: 400 .75rem var(--sub);
  text-transform: uppercase;
  user-select: none
}

.avatar-mono {
  background: var(--soft-sage);
  color: var(--c-text)
}

.avatar-md {
  width: 52px;
  height: 52px;
  font-size: 1.2rem
}

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

.dtable-avatar {
  width: 1%;
  padding-right: 0
}

.avatar-sm {
  width: 22px;
  height: 22px;
  font-size: .66rem
}

.shell-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 12px);
  width: 205px;
  padding: .5rem;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  box-shadow: 5px 5px 0 var(--c-border);
  z-index: 3
}

.shell-menu a,
.shell-menu button {
  display: flex;
  width: 100%;
  gap: .5rem;
  padding: .5rem;
  border: 0;
  background: none;
  color: var(--c-text);
  font: inherit;
  text-align: left
}

.shell-menu a:hover,
.shell-menu button:hover {
  background: var(--c-surface-2)
}

.shell-menu hr {
  border: 0;
  border-top: 1px solid var(--c-border)
}

.shell-menu form {
  margin: 0
}

.shell-rail {
  grid-area: rail;
  display: flex;
  flex-direction: column;
  gap: .2rem;
  padding: 1rem .8rem;
  background: var(--c-rail)
}

.shell-rail a,
.rail-off {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .6rem .8rem;
  border-radius: 999px;
  color: var(--c-on-dark-muted);
  text-decoration: none;
  font-size: .88rem
}

.shell-rail a:hover {
  background: var(--c-rail-hover);
  color: var(--c-on-dark)
}

.shell-rail a[aria-current=page] {
  background: var(--vibrant-yellow);
  color: var(--c-text);
  font-weight: 400
}

.rail-off {
  opacity: .42
}

.shell-rail-label {
  margin-top: 1rem;
  padding: .8rem .75rem .3rem;
  border-top: 1px solid var(--c-line-on-dark);
  color: var(--c-on-dark-muted);
  font: .62rem var(--mono);
  letter-spacing: .12em;
  text-transform: uppercase
}

.shell-rail-version {
  margin-top: auto;
  color: var(--c-on-dark-muted);
  font: .68rem var(--mono);
  padding: .7rem
}

/* Lives in .shell-brand (sticky, always visible) rather than at the bottom
   of the rail -- a control needed on every click has to stay reachable
   without scrolling, which the rail's own bottom is not once its link list
   plus the version string outgrow a short viewport. */
.rail-collapse-btn {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  padding: .4rem;
  margin: 0 0 0 auto;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--c-on-dark-muted);
  cursor: pointer
}

.rail-collapse-btn:hover {
  background: var(--c-rail-hover);
  color: var(--c-on-dark)
}

/* Desktop sidebar collapse (persisted via the rail_collapsed cookie, applied
   server-side on the next page load, toggled instantly by console.js). Only
   the icon column stays -- labels and section headers hide; a tooltip (the
   `title` attribute already on every link) still names each icon on hover. */
.rail-collapsed .shell-rail a,
.rail-collapsed .rail-off {
  justify-content: center;
  padding: .65rem
}

.rail-collapsed .rail-label,
.rail-collapsed .shell-rail-label,
.rail-collapsed .brand-wordmark {
  display: none
}

/* The logo and the collapse button used to sit side by side here -- together
   they're wider than the collapsed rail, so they overlapped. Stacked instead,
   both shrunk a little so the pair still fits the fixed 62px brand row. */
.rail-collapsed .shell-brand {
  flex-direction: column;
  justify-content: center;
  padding: 0;
  gap: .2rem
}

.rail-collapsed .shell-brand .brand-full {
  display: none
}

.rail-collapsed .shell-brand .brand-symbol {
  display: block;
  width: auto;
  height: 22px
}

.rail-collapsed .rail-collapse-btn {
  margin: 0;
  padding: .15rem
}

.rail-collapsed .rail-collapse-btn svg {
  width: 18px;
  height: 18px;
  transform: rotate(180deg)
}

.shell-main {
  grid-area: main;
  padding: clamp(1.3rem, 3vw, 3rem)
}

.wrap {
  max-width: 1300px;
  margin: auto
}

/* The IoT Dashboard console is an embedded app, not a reading-width page --
   let it use the full width .shell-main gives it (more so once the sidebar
   is collapsed too). */
.wrap-wide {
  max-width: none
}

.page-top {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  align-items: start;
  margin-bottom: 1.7rem;
  padding-bottom: 1rem;
  border-bottom: 2px solid var(--c-text)
}

.page-top h1 {
  font-size: clamp(1.8rem, 3vw, 2.7rem);
  margin: 0
}

.page-top-id {
  display: flex;
  align-items: center;
  gap: 1rem
}

.avatar-edit {
  display: flex;
  align-items: start;
  gap: 1.2rem;
  flex-wrap: wrap
}

.avatar-edit .form {
  margin-bottom: .6rem
}

/* Small Deep Teal pill label (brand guide p. 2, 5, 7). */
.eyebrow {
  display: inline-block;
  margin: 0 0 .5rem;
  padding: .15rem .65rem;
  border-radius: 999px;
  background: var(--c-text);
  color: var(--c-on-dark);
  font: 400 .72rem var(--sub);
  letter-spacing: .02em
}

.meta,
.muted {
  color: var(--c-muted)
}

.danger-text {
  color: var(--c-bad)
}

.page-actions,
.admin-actions,
.kv-actions {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap
}

.tiles {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--c-border);
  margin-bottom: 1.4rem
}

.tile {
  background: var(--c-surface);
  padding: 1rem
}

.tile-label {
  color: var(--c-muted);
  font: .65rem var(--mono);
  letter-spacing: .08em;
  text-transform: uppercase
}

.tile-value {
  display: block;
  font: 400 1.9rem var(--display);
  letter-spacing: -.07em
}

.tile-sub {
  font-size: .78rem;
  color: var(--c-muted)
}

/* prominent "credits left" readout on the SMS console */
.credit-meter {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: .4rem;
  margin: .1rem 0 .4rem
}

.credit-meter b {
  font: 400 2.1rem var(--display);
  line-height: 1;
  letter-spacing: -.04em;
  color: var(--c-teal)
}

.credit-meter span {
  font-size: .84rem;
  color: var(--c-muted)
}

.credit-meter-low b {
  color: var(--c-bad)
}

.credit-bar {
  height: 7px;
  max-width: 360px;
  margin: 0 0 .3rem;
  background: var(--c-surface-2);
  border: 1px solid var(--c-border);
  overflow: hidden
}

/* the fill is the credits that REMAIN, so it shrinks as they are spent */
.credit-bar > i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--c-teal);
  transition: width .25s ease
}

.credit-bar-low > i {
  background: var(--c-bad)
}

.credit-used {
  margin: 0 0 .7rem;
  font-size: .78rem
}

/* the verified-number status line on the SMS console */
.sms-number-line {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
  margin: .1rem 0 .5rem
}

.sms-number-line .muted {
  font-size: .8rem
}

.sms-number {
  font: 600 1rem var(--mono);
  letter-spacing: .02em;
  color: var(--c-text)
}

/* CSP blocks inline style attributes, so the fill width is a bucketed class.
   Selector must out-specify `.credit-bar > i` above (which sets width:0). */
.credit-bar > i.cb-w0 { width: 0 }
.credit-bar > i.cb-w10 { width: 10% }
.credit-bar > i.cb-w20 { width: 20% }
.credit-bar > i.cb-w30 { width: 30% }
.credit-bar > i.cb-w40 { width: 40% }
.credit-bar > i.cb-w50 { width: 50% }
.credit-bar > i.cb-w60 { width: 60% }
.credit-bar > i.cb-w70 { width: 70% }
.credit-bar > i.cb-w80 { width: 80% }
.credit-bar > i.cb-w90 { width: 90% }
.credit-bar > i.cb-w100 { width: 100% }

.pill-strong {
  font-weight: 700;
  color: var(--c-teal);
  border-color: var(--c-teal);
  background: var(--c-teal-dim)
}

/* contact picker: a styled dropdown over the recipient box (sms.js) */
.combo {
  position: relative
}

.combo-list {
  position: absolute;
  z-index: 30;
  left: 0;
  right: 0;
  margin: 3px 0 0;
  padding: 0;
  list-style: none;
  max-height: 15rem;
  overflow-y: auto;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  box-shadow: 0 8px 22px rgba(24, 47, 54, .16)
}

.combo-list li {
  padding: .5rem .7rem;
  cursor: pointer;
  font-size: .88rem;
  border-bottom: 1px solid var(--c-border-soft)
}

.combo-list li:last-child {
  border-bottom: 0
}

.combo-list li:hover,
.combo-list li[aria-selected="true"] {
  background: var(--c-teal-dim)
}

.combo-num {
  color: var(--c-muted);
  font: .8rem var(--mono)
}

/* live "what the recipient gets" preview under the SMS message box */
.sms-preview {
  margin: .4rem 0 0;
  border: 1px solid var(--c-border);
  background: var(--warm-ivory)
}

.sms-preview-label {
  display: block;
  padding: .3rem .55rem;
  border-bottom: 1px solid var(--c-border);
  font: .58rem var(--mono);
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--c-muted)
}

.sms-preview pre {
  margin: 0;
  padding: .6rem .55rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font: .84rem var(--mono);
  color: var(--c-text)
}

.sms-preview-added {
  color: var(--c-muted)
}

.grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.2rem
}

/* The FUOTA console's live-refreshing panels sit in their own .grid inside
   #fuota-live, ahead of the static config .grid -- without this the two
   read as one merged block, since a grid's own `gap` only spaces rows
   within itself, not between separate grid containers. */
#fuota-live + .grid {
  margin-top: 1.2rem
}

/* Let each panel keep its natural height instead of stretching to match its
   row-mate -- for a form beside a long log (the SMS console), the form should
   not grow to the log's height. */
.grid-top {
  align-items: start
}

/* keep a wide child (a resized textarea, a long token) from stretching its
   grid track and scrolling the page sideways */
.grid > * {
  min-width: 0
}

.col-2 {
  grid-column: 1/-1
}

.panel {
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: 1rem;
  overflow: hidden
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .8rem 1rem;
  border-bottom: 1px solid var(--c-border)
}

.panel-head h2 {
  margin: 0;
  font-size: .95rem
}

.panel-body {
  padding: 1rem
}

.panel-empty {
  padding: 1rem 0;
  color: var(--c-muted);
  border-left: 2px solid var(--c-orange);
  padding-left: .7rem
}

.dtable {
  width: 100%;
  border-collapse: collapse;
  font-size: .84rem
}

.dtable th {
  text-align: left;
  color: var(--c-muted);
  font: .63rem var(--mono);
  text-transform: uppercase;
  letter-spacing: .1em
}

.dtable th,
.dtable td {
  padding: .65rem;
  border-bottom: 1px solid var(--c-border-soft)
}

.dtable tr:last-child td {
  border: 0
}

.dtable tbody tr:hover {
  background: var(--c-surface-2)
}

.dtable tbody tr.hl td {
  background: var(--c-warn-dim);
  font-weight: 600
}

/* Gateway connection settings: the handful of values a customer types into
   their gateway. The card confirms; the <mark> chips make the address and
   port impossible to miss or misread. */
.gw-conn {
  margin-bottom: 1rem;
  padding: .85rem 1rem;
  background: var(--c-ok-dim);
  border: 1px solid var(--c-ok);
  border-left-width: 3px
}

.gw-conn-lead {
  margin: 0 0 .75rem;
  font-size: .9rem;
  color: var(--c-text)
}

.gw-conn-lead .ci {
  vertical-align: -.15em;
  margin-right: .25rem;
  color: var(--c-ok)
}

.gw-conn-lead code {
  padding: 0 .25rem;
  background: var(--c-surface);
  border: 1px solid var(--c-border-soft)
}

.kv-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: .5rem 1rem;
  align-items: center;
  margin: 0
}

.kv-grid dt {
  color: var(--c-muted);
  font-size: .8rem
}

.gw-conn-sub {
  color: var(--c-faint)
}

.kv-grid dd {
  margin: 0
}

/* a row whose dd holds a multi-line block (e.g. a <details>), not a single
   line of text like the others -- center-aligning it against the dt looks
   like the two are floating apart, so pin both to the top instead. */
.kv-grid dt.kv-grid-top,
.kv-grid dd.kv-grid-top {
  align-self: start
}

.kv-grid mark,
.gw-conn-tbl mark {
  display: inline-block;
  padding: .15rem .55rem;
  background: var(--c-warn-dim);
  color: var(--c-warn);
  border: 1px solid var(--c-warn);
  font: 700 1rem var(--mono);
  letter-spacing: .02em
}

.gw-conn-tbl {
  width: 100%;
  border-collapse: collapse;
  font-size: .84rem
}

.gw-conn-tbl th {
  text-align: left;
  color: var(--c-muted);
  font: .63rem var(--mono);
  text-transform: uppercase;
  letter-spacing: .1em
}

.gw-conn-tbl th,
.gw-conn-tbl td {
  padding: .45rem .55rem .45rem 0
}

.gw-conn-tbl mark {
  font-size: .9rem
}

.cert-pre {
  margin: 0 0 .5rem;
  padding: .6rem .7rem;
  max-height: 12rem;
  overflow: auto;
  white-space: pre;
  font-size: .72rem;
  line-height: 1.35;
  background: var(--c-surface);
  border: 1px solid var(--c-border-soft)
}

/* LNS console: three panels for three different concerns -- space them out
   and colour-code each so they don't read as one wall. */
.lns-panels,
.lns-panels #lns-live,
.lns-panels #influx-live {
  display: flex;
  flex-direction: column;
  gap: 1.6rem
}

.lns-card {
  border-left: 3px solid var(--c-border)
}

.lns-card > .panel-head {
  background: var(--c-surface-2)
}

.lns-card--auth {
  border-left-color: var(--c-teal)
}

.lns-card--auth > .panel-head h2 .ci {
  color: var(--c-teal)
}

.lns-card--live {
  border-left-color: var(--c-orange)
}

.lns-card--live > .panel-head h2 .ci {
  color: var(--c-text)
}

.iot-embed-panel {
  margin-bottom: 1.6rem
}

.iot-embed {
  display: block;
  width: 100%;
  height: calc(100vh - 190px);
  min-height: 760px;
  border: 0
}

@media(max-width:850px) {
  .iot-embed {
    height: calc(100vh - 260px);
    min-height: 480px
  }
}

.lns-card--port {
  border-left-color: var(--c-warn)
}

.lns-card--port > .panel-head h2 .ci {
  color: var(--c-warn)
}

/* "Your ChirpStack" live overview */
.ov-tiles {
  grid-template-columns: repeat(3, 1fr);
  margin-bottom: 1.1rem
}

.ov-section {
  border: 1px solid var(--c-border);
  margin-bottom: .8rem
}

.ov-section > h3 {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: 0;
  padding: .5rem .75rem;
  background: var(--c-surface-2);
  border-bottom: 1px solid var(--c-border);
  font: 600 .68rem var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--c-muted)
}

.ov-section > h3 .ci {
  width: 15px;
  height: 15px;
  color: var(--c-teal)
}

.ov-section-body {
  padding: .3rem .75rem
}

.ov-section .dtable {
  font-size: .82rem
}

.ov-empty {
  margin: .55rem 0;
  color: var(--c-muted);
  font-size: .85rem
}

.ov-foot {
  margin: .9rem 0 0;
  color: var(--c-faint);
  font-size: .78rem
}

.ov-foot [data-live-status] {
  color: var(--c-ok)
}

.table-scroll,
.sms-log {
  overflow: auto
}

.scroll-y {
  max-height: 20rem;
  overflow: auto
}

.kv {
  display: grid;
  grid-template-columns: max-content 1fr auto;
  gap: .55rem 1rem
}

.kv dt {
  color: var(--c-muted);
  font-size: .8rem
}

.kv dd {
  margin: 0
}

.kv-act {
  display: flex;
  gap: .3rem;
  align-items: start
}

/* a text input with its Generate / Copy buttons beside it */
.input-action {
  display: flex;
  gap: .4rem;
  align-items: center
}

.input-action input {
  flex: 1;
  min-width: 0
}

.pill,
.sms-status {
  display: inline-flex;
  padding: .15rem .45rem;
  font: .68rem var(--mono);
  background: var(--c-surface-2);
  border: 1px solid var(--c-border)
}

.pill-ok,
.sms-status-sent,
.sms-status-delivered {
  background: var(--c-ok-dim);
  color: var(--c-ok)
}

.pill-warn,
.sms-status-pending,
.sms-status-queued {
  background: var(--c-warn-dim);
  color: var(--c-warn)
}

.pill-bad,
.sms-status-failed {
  background: var(--c-bad-dim);
  color: var(--c-bad)
}

.dot:before {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: .4rem;
  background: var(--c-faint)
}

.dot-ok:before,
.dot-live:before {
  background: var(--c-ok)
}

.dot-warn:before {
  background: var(--c-warn)
}

.dot-bad:before {
  background: var(--c-bad)
}

.field {
  margin-bottom: 1rem
}

/* A 6-digit code split into one box per digit (console.js), auto-advancing
   and auto-submitting once filled -- the underlying <input> becomes the
   hidden field that actually gets posted. */
.otp-boxes {
  display: flex;
  gap: .5rem;
  justify-content: center;
  margin: .3rem 0
}

.otp-box {
  width: 2.5rem;
  height: 3rem;
  padding: 0;
  text-align: center;
  font: 600 1.4rem var(--mono)
}

/* Stacked forms / action rows in a panel would otherwise render flush against
   the block above them (their buttons touch). Space any form, or a button row,
   that follows another one. Horizontal button rows (.admin-actions /
   .page-actions) space their own children with flex `gap`, so a form *inside*
   one is reset back to 0. */
.panel-body form + form,
.panel-body .form + .admin-actions,
.panel-body .admin-actions + form,
.panel-body .admin-actions + .admin-actions,
.panel-body .kv-grid + .admin-actions,
.avatar-edit form + form {
  margin-top: 1rem
}

/* A modal <dialog> is display:none while JS runs but is still a DOM sibling,
   so it breaks the "+" spacing between the action rows on either side of it. */
.panel-body dialog.modal + .admin-actions,
.panel-body dialog.modal + .form,
.panel-body dialog.modal + form {
  margin-top: 1rem
}

.admin-actions form + form,
.page-actions form + form {
  margin-top: 0
}

.field label {
  display: block;
  margin-bottom: .3rem;
  font: 400 .76rem var(--sub)
}

.hint,
.form-note {
  color: var(--c-muted);
  font-size: .8rem
}

details.admin-sub,
.admin-sub {
  margin: .2rem 0 1rem
}

details.admin-sub > summary {
  cursor: pointer;
  margin-bottom: .6rem;
  font: 400 .8rem var(--sub);
  color: var(--c-teal)
}

input[type=text],
input[type=email],
input[type=password],
input[type=number],
input[type=search],
input[type=file],
input[type=tel],
input[type=url],
select,
textarea {
  width: 100%;
  padding: .65rem .7rem;
  border: 1px solid color-mix(in srgb, var(--deep-teal) 45%, transparent);
  border-radius: .6rem;
  background: var(--warm-ivory);
  color: var(--c-text);
  font: inherit
}

.form-inline {
  display: flex;
  gap: .5rem;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 1rem
}

.form-inline input[type=search] {
  width: auto;
  flex: 1 1 16rem
}

textarea {
  min-height: 7rem;
  /* grow downward only -- dragging it wider would push the grid and scroll
     the page sideways */
  resize: vertical;
  max-width: 100%
}

input:focus,
select:focus,
textarea:focus {
  outline: 3px solid var(--c-teal-dim);
  border-color: var(--c-teal)
}

.inline-form {
  display: flex;
  gap: .6rem;
  align-items: end;
  flex-wrap: wrap
}

.inline-form .field {
  flex: 1;
  margin: 0
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  border: 1px solid var(--c-text);
  background: var(--c-text);
  color: var(--c-surface);
  padding: .55rem 1rem;
  border-radius: 999px;
  cursor: pointer;
  font: 400 .8rem var(--sub);
  text-decoration: none
}

.btn:hover {
  background: var(--c-orange);
  border-color: var(--c-orange);
  color: var(--c-text)
}

.btn:disabled {
  opacity: .6
}

/* console.js marks a submitting button aria-busy (and disables it) so a
   slow request (an upload, a provider API call) reads as "working", not
   "did my click even land" -- a small spinner in front of the label is
   the visible half of that. Any icon in the button (.ci) steps aside so
   the two don't crowd a tight icon-only button. */
.btn[aria-busy="true"] {
  cursor: wait
}

.btn[aria-busy="true"] .ci {
  display: none
}

.btn[aria-busy="true"]::before {
  content: "";
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: btn-spin .6s linear infinite
}

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

@media(prefers-reduced-motion:reduce) {
  .btn[aria-busy="true"]::before {
    animation: none
  }
}

.btn-primary {
  background: var(--c-orange);
  border-color: var(--c-orange);
  color: var(--c-text)
}

.btn-ghost {
  background: transparent;
  color: var(--c-text)
}

.btn-danger {
  background: var(--c-bad-dim);
  color: var(--c-bad);
  border-color: var(--c-bad)
}

.btn-sm {
  padding: .35rem .75rem
}

.flash,
.form-error,
.form-ok,
.inline-note {
  padding: .7rem .8rem;
  margin-bottom: 1rem;
  border-left: 3px solid
}

.flash-ok,
.form-ok {
  background: var(--c-ok-dim);
  color: var(--c-ok)
}

.flash-err,
.form-error {
  background: var(--c-bad-dim);
  color: var(--c-bad)
}

.flash-warn,
.inline-note {
  background: var(--c-warn-dim);
  color: var(--c-warn)
}

.tabs,
.sms-tabs {
  display: flex;
  gap: 1rem;
  border-bottom: 1px solid var(--c-border);
  margin-bottom: 1.3rem
}

.tabs a,
.sms-tabs a,
.tabs .tab-btn {
  padding: .5rem 0;
  color: var(--c-muted);
  text-decoration: none
}

.tabs a[aria-current=page],
.sms-tabs a[aria-current=page],
.tabs .tab-btn[aria-selected=true] {
  color: var(--c-text);
  border-bottom: 3px solid var(--c-text)
}

.tab-locked {
  padding: .5rem 0;
  color: var(--c-faint);
  cursor: not-allowed
}

/* Tab buttons (as opposed to the .tabs/.sms-tabs <a> variant, which
   navigates between real pages): same look, but a plain in-page toggle --
   strip the button chrome and add back the same underline-on-select as
   [aria-current=page] gets. Without JS these are inert and every panel
   below just renders open, same as before tabs existed -- see console.js. */
.tabs .tab-btn {
  background: none;
  border: none;
  border-bottom: 3px solid transparent;
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: .35rem
}

/* JS-only: console.js adds [hidden] to every non-active panel once it has
   picked one on load. .grid's own `display: grid` (an author rule) would
   otherwise win over the UA [hidden]{display:none} rule at equal specificity,
   so this needs to be spelled out explicitly. */
.grid[data-tab-panel][hidden] {
  display: none
}

.session-list {
  list-style: none;
  padding: 0;
  margin: .6rem 0 1rem;
  border-top: 1px solid var(--c-border-soft)
}

.session-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .5rem 1rem;
  padding: .8rem 0;
  border-bottom: 1px solid var(--c-border-soft)
}

.session-list .session-device {
  display: block;
  font-weight: 600
}

.session-list .session-sub {
  display: block;
  font-size: .82rem;
  color: var(--c-muted)
}

.svc-list {
  padding: 0;
  list-style: none
}

.svc-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .5rem .75rem;
  border-bottom: 1px solid var(--c-border);
  padding: .8rem 0
}

.svc-name {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-weight: 500
}

/* Everything after the service name -- status pills then the action button,
   or the trial-activation form -- lives here and stays right-aligned so the
   rows line up whatever each service's state is. */
.svc-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: .4rem .5rem;
  margin-left: auto
}

/* The activation form flows its parts into the same row rather than forming
   a nested box, so a plain "Activate" button aligns like "Open console". */
.svc-actions > form {
  display: contents
}

.svc-actions .form-note {
  flex-basis: 100%;
  text-align: left
}

/* The LNS form also asks for a gateway EUI + region: drop the whole action
   group onto its own full-width line under the service name. */
.svc-list li:has(.admin-sub) {
  align-items: flex-start
}

.svc-actions:has(.admin-sub) {
  flex-basis: 100%;
  margin-left: 0;
  margin-top: .4rem;
  justify-content: flex-start
}

.svc-actions .admin-sub {
  flex-basis: 100%
}

.auth-wrap {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 2rem;
  background: var(--c-rail)
}

.auth-box {
  width: min(100%, 440px)
}

/* Logo 40px tall; X = 13px clear space, kept by the 1.2rem gap. */
.auth-box .brand {
  display: flex;
  align-items: center;
  gap: 1.2rem;
  margin-bottom: 1.6rem;
  color: var(--c-on-dark);
  text-decoration: none
}

.auth-box .brand img {
  display: block;
  height: 40px;
  width: auto
}

.brand-tag {
  padding: .15rem .7rem;
  border: 1px solid var(--c-line-on-dark);
  border-radius: 999px;
  font: 400 .8rem var(--sub)
}

.auth-card {
  background: var(--c-surface);
  padding: clamp(1.3rem, 5vw, 2.2rem);
  border-radius: 1.25rem
}

.auth-card h1 {
  margin-top: 0
}

.auth-alt {
  text-align: center;
  color: var(--c-muted);
  font-size: .84rem
}

.btn-lg {
  padding: .72rem 1rem;
  font-size: .85rem
}

/* auth card: full-width stacked buttons (Google / email / submit) */
.auth-card .btn {
  display: flex;
  width: 100%
}

.auth-card > .btn + .btn,
.auth-card > form + form,
.auth-card > form + .btn,
.auth-card > .btn + form {
  margin-top: .6rem
}

.auth-divider {
  display: flex;
  align-items: center;
  gap: .8rem;
  margin: 1.4rem 0;
  color: var(--c-muted);
  font-size: .78rem
}

.auth-divider:before,
.auth-divider:after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--c-border)
}

.rail-toggle,
.rail-toggle-btn {
  display: none
}

@media(max-width:850px) {
  .shell {
    display: block
  }

  /* The desktop collapse (a grid-column trick) doesn't apply once .shell is
     just a block -- the off-canvas rail toggle already handles small screens,
     so undo every collapsed-state effect and hide its own button here. */
  .rail-collapse-btn {
    display: none
  }

  .rail-collapsed .shell-rail a,
  .rail-collapsed .rail-off {
    justify-content: flex-start;
    padding: .65rem .75rem
  }

  .rail-collapsed .rail-label,
  .rail-collapsed .shell-rail-label,
  .rail-collapsed .brand-wordmark {
    display: unset
  }

  .rail-collapsed .shell-brand {
    flex-direction: row;
    justify-content: flex-start;
    padding: 0 1.2rem
  }

  .rail-collapsed .shell-brand img {
    width: 26px;
    height: 26px
  }

  /* the thin logo bar can scroll away on a phone; the top bar (context +
     account chip) stays pinned from the base rule */
  .shell-brand {
    height: 55px;
    position: static
  }

  .shell-topbar {
    height: 54px;
    padding: 0 1rem
  }

  .shell-rail {
    position: fixed;
    z-index: 4;
    top: 55px;
    bottom: 0;
    width: 250px;
    transform: translateX(-102%);
    transition: transform .18s
  }

  .rail-toggle:checked~.shell .shell-rail {
    transform: none
  }

  .rail-toggle-btn {
    display: grid;
    place-items: center
  }

  .rail-scrim {
    display: none
  }

  .rail-toggle:checked~.rail-scrim {
    display: block;
    position: fixed;
    inset: 0;
    background: #152d3588;
    z-index: 3
  }

  .shell-main {
    padding: 1rem
  }

  .tiles,
  .grid {
    grid-template-columns: 1fr
  }

  .col-2 {
    grid-column: auto
  }

  .shell-user summary span:last-child {
    display: none
  }
}

@media(max-width:500px) {
  .kv {
    grid-template-columns: 1fr
  }

  .page-top {
    display: block
  }

  .page-actions {
    margin-top: 1rem
  }
}

/* --- modal dialogs (native <dialog>) --------------------------------
   Opened by console.js from a [data-dialog="id"] trigger. With JS off, the
   <noscript> block in app.html renders every .modal inline and hides the
   triggers, so the form inside still works. */
dialog.modal {
  width: min(30rem, calc(100vw - 2rem));
  max-height: calc(100dvh - 4rem);
  padding: 0;
  border: 1px solid var(--c-border);
  border-radius: 10px;
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: 0 16px 48px rgba(24, 47, 54, .35)
}

dialog.modal::backdrop {
  background: rgba(24, 47, 54, .55)
}

.modal-head {
  padding: .95rem 1.15rem;
  border-bottom: 1px solid var(--c-border-soft);
  font: 400 1rem var(--sub)
}

.modal-body {
  padding: 1.15rem
}

.modal-body > p:first-child {
  margin-top: 0
}

.modal-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .6rem;
  justify-content: flex-end;
  margin-top: 1.1rem
}

/* Blog admin: cover/thumbnail previews, image-insert toolbar. */
.news-cover-preview {
  display: block;
  max-width: 420px;
  width: 100%;
  border: 1px solid var(--c-border);
  margin-bottom: .75rem
}

.news-thumb {
  width: 64px;
  height: 64px;
  object-fit: cover;
  border: 1px solid var(--c-border);
  display: block
}

.news-editor-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .6rem;
  margin-bottom: .5rem
}
