/* ============================================================
   yesIT Customer Portal - component layer
   ------------------------------------------------------------
   Loaded after yesit-tokens.css and after Bootstrap. Small on purpose:
   the token layer re-points Bootstrap's own --bs-* variables to the brand
   palette, so stock Bootstrap cards, tables and buttons already render in
   brand colours and need no overrides here.

   This file covers only what Bootstrap has no equivalent for: the branded
   loading spinner and the portal header.
   ============================================================ */

body {
  background: var(--bg-body, #f6f8fa);
  color: var(--text-primary, #102547);
  font-family: var(--font-sans, system-ui, sans-serif);
}

/* ===== Header =====
   Mirrors yesit.com.au: a Deep Core band with the reversed logo and the
   "Get in touch" number on the right, so a customer arriving from the
   website recognises where they are before reading anything. */

.yesit-header {
  background-color: var(--yesit-deep-core, #102547);
  color: #fff;
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

/* The tech-triangle motif, faint, off the right edge - the treatment the
   internal app gives its navbar and the website gives its dark bands. */
.yesit-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("../images/patterns/polygons-datastream.68b9035dd6c7.svg");
  background-repeat: no-repeat;
  background-position: right -60px center;
  background-size: auto 320%;
  opacity: 0.3;
}

.yesit-header-inner {
  display: flex;
  align-items: center;
  gap: var(--space-5, 1.5rem);
  padding-top: var(--space-3, 0.75rem);
  padding-bottom: var(--space-3, 0.75rem);
  flex-wrap: wrap;
}

.yesit-header-logo {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
}

.yesit-header-nav {
  display: flex;
  gap: var(--space-4, 1rem);
}

.yesit-header-nav a {
  color: rgba(255, 255, 255, 0.82);
  text-decoration: none;
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-weight: var(--fw-medium, 500);
  padding: 0.25rem 0;
  border-bottom: 2px solid transparent;
}

.yesit-header-nav a:hover,
.yesit-header-nav a.active {
  color: #fff;
  border-bottom-color: var(--yesit-data-stream, #53c6d6);
}

.yesit-header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3, 0.75rem);
  margin-left: auto;
}

.yesit-contact {
  color: rgba(255, 255, 255, 0.82);
  font-size: 0.9rem;
}

/* The number in Data Stream teal, exactly as the website renders it. */
.yesit-contact a {
  color: var(--yesit-data-stream, #53c6d6);
  font-weight: var(--fw-semibold, 600);
  text-decoration: none;
}

.yesit-customer {
  color: #fff;
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-weight: var(--fw-medium, 500);
  padding-left: var(--space-3, 0.75rem);
  border-left: 1px solid rgba(255, 255, 255, 0.22);
}

.yesit-header-btn {
  background: transparent;
  color: rgba(255, 255, 255, 0.88);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: var(--radius-sm, 4px);
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: 0.8125rem;
  font-weight: var(--fw-medium, 500);
  padding: 0.3rem 0.75rem;
}

.yesit-header-btn:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: var(--yesit-data-stream, #53c6d6);
  color: #fff;
}

/* ===== Telemetry charts =====
   Server-rendered SVG rather than a charting library: the CSP admits scripts
   from cdnjs only, nothing here needs interactivity, and a <polyline> can be
   asserted on in a test where a canvas cannot. Two columns on desktop,
   one on a phone, matching the layout customers already know from Starlink's
   own portal. */

.chart-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-4, 1.5rem);
}

.chart figcaption {
  display: flex;
  flex-direction: column;
  margin-bottom: 0.35rem;
}

.chart-label {
  color: var(--text-body);
  font-size: 0.8125rem;
}

.chart-summary {
  color: var(--text-strong);
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-weight: var(--fw-medium, 500);
  font-size: 0.9375rem;
}

/* preserveAspectRatio="none" on the SVG lets the viewBox stretch to the
   column; the stroke keeps its width via vector-effect on the polyline. */
.chart-svg {
  display: block;
  width: 100%;
  height: 110px;
  overflow: visible;
}

.chart-grid-line {
  stroke: var(--border-subtle, rgba(128, 128, 128, 0.25));
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

.chart-line {
  fill: none;
  stroke: var(--accent, #53c6d6);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Hover readout. The figure is the positioning context so the tooltip can
   follow the cursor horizontally without escaping the chart. */
.chart {
  position: relative;
}

.chart-cursor {
  stroke: var(--text-muted, #5a6b82);
  stroke-width: 1;
  stroke-dasharray: 3 3;
}

.chart-dot {
  fill: var(--accent, #53c6d6);
  stroke: var(--surface-card, #fff);
  stroke-width: 2;
}

.chart-tip {
  position: absolute;
  bottom: 1.6rem;
  transform: translateX(-50%);
  z-index: 2;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.35rem 0.6rem;
  border-radius: var(--radius-sm, 4px);
  background: var(--surface-inverse, #102547);
  color: var(--text-on-dark, #fff);
  font-size: 0.8125rem;
  white-space: nowrap;
  box-shadow: 0 4px 12px rgb(16 37 71 / 25%);
}

.chart-tip-time {
  opacity: 0.75;
  font-size: 0.75rem;
}

.chart-tip-value {
  font-weight: var(--fw-medium, 500);
}

/* Monthly usage beside the current cycle. Bars are relative to the tallest
   month, not the allowance: an unlimited plan has no allowance to scale to. */
.cycle-strip {
  display: flex;
  align-items: flex-end;
  gap: 0.35rem;
  height: 72px;
}

.cycle {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  min-width: 0;
}

.cycle-bar {
  display: flex;
  align-items: flex-end;
  width: 100%;
  height: 52px;
  background: var(--surface-subtle, #f4f6f8);
  border-radius: 2px;
  overflow: hidden;
}

.cycle-bar span {
  display: block;
  width: 100%;
  /* A month with no usage still shows a sliver, so the bar reads as "nothing"
     rather than as a month we have no data for. */
  min-height: 2px;
  background: var(--accent, #53c6d6);
}

.cycle-label {
  color: var(--text-body);
  font-size: 0.6875rem;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chart-axis {
  display: flex;
  justify-content: space-between;
  color: var(--text-body);
  font-size: 0.6875rem;
  margin-top: 0.15rem;
}

/* ===== Staff preview banner =====
   Only rendered when a staff member has opened the portal as a customer.
   Warning colours rather than brand ones on purpose: this is the one strip
   of the page that must not look like part of the product, so that nobody
   mistakes a preview for the customer's own session - or a screenshot of one
   for evidence of what the customer did. Both themes are defined, because a
   banner that vanishes into a dark background defeats the point. */

.yesit-preview-banner {
  background: var(--warning-bg, #fff3cd);
  color: var(--warning-text, #856404);
  border-bottom: 2px solid var(--warning-border, #d9a134);
  font-size: 0.875rem;
}

.yesit-preview-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3, 1rem);
  flex-wrap: wrap;
  padding-block: 0.5rem;
}

.yesit-preview-btn {
  background: transparent;
  color: inherit;
  border: 1px solid currentcolor;
  border-radius: var(--radius-sm, 4px);
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: 0.8125rem;
  font-weight: var(--fw-medium, 500);
  padding: 0.25rem 0.7rem;
  white-space: nowrap;
}

.yesit-preview-btn:hover {
  background: var(--warning-border, #d9a134);
  border-color: var(--warning-border, #d9a134);
  color: var(--yesit-deep-core, #102547);
}

/* ===== Footer =====
   The website's contact block rather than its marketing link columns: a
   customer looking at a service problem wants the support number and the
   hours, not a services menu. */

.yesit-footer {
  background-color: var(--yesit-deep-core, #102547);
  color: rgba(255, 255, 255, 0.78);
  position: relative;
  isolation: isolate;
  overflow: hidden;
  margin-top: var(--space-8, 4rem);
  padding-top: var(--space-6, 2rem);
  font-size: 0.875rem;
}

.yesit-footer::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("../images/patterns/polygons-datastream.68b9035dd6c7.svg");
  background-repeat: no-repeat;
  background-position: left -180px center;
  background-size: auto 190%;
  opacity: 0.18;
}

.yesit-footer-inner {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6, 2rem);
  padding-bottom: var(--space-5, 1.5rem);
}

.yesit-footer-brand {
  flex: 1 1 14rem;
}

.yesit-footer-contact,
.yesit-footer-address {
  flex: 0 1 16rem;
}

.yesit-footer a {
  color: var(--yesit-data-stream, #53c6d6);
  text-decoration: none;
}

.yesit-footer a:hover {
  text-decoration: underline;
}

/* Eyebrow on a dark ground: teal rather than the muted grey used on light. */
.eyebrow-light {
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  font-weight: var(--fw-semibold, 600);
  font-size: var(--fs-xs, 0.75rem);
  font-family: var(--font-brand, "Poppins", sans-serif);
  color: var(--yesit-data-stream, #53c6d6);
}

.yesit-footer-legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-3, 0.75rem);
  padding-top: var(--space-4, 1rem);
  padding-bottom: var(--space-5, 1.5rem);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  font-size: 0.8125rem;
  color: rgba(255, 255, 255, 0.6);
}

/* ===== Sign-in hero =====
   The website's hero photograph under a Deep Core wash, so the sign-in page
   reads as the same property rather than a bolted-on login form. */

.yesit-hero {
  position: relative;
  min-height: 32rem;
  display: flex;
  align-items: center;
  background-image: linear-gradient(
      to right,
      rgba(16, 37, 71, 0.94) 0%,
      rgba(16, 37, 71, 0.72) 55%,
      rgba(16, 37, 71, 0.55) 100%
    ),
    var(--yesit-hero-image);
  background-size: cover;
  background-position: center;
  color: #fff;
}

.yesit-hero h1 {
  color: #fff;
}

.yesit-hero .eyebrow-light {
  display: block;
  margin-bottom: var(--space-3, 0.75rem);
}

.yesit-hero-lead {
  color: rgba(255, 255, 255, 0.85);
  max-width: 34rem;
}

/* ===== Branded loading spinner =====
   The rotating mark, used wherever a panel or table is fetching. Plain
   "Loading..." text is not used anywhere in the portal; terminal states
   ("No services", "Failed to load") are plain text instead. */

.logo-spinner-overlay {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 2rem 1rem;
}

.logo-spinner-overlay.fullscreen {
  position: fixed;
  inset: 0;
  z-index: 1080;
  background: color-mix(in srgb, var(--bg-body, #f6f8fa) 88%, transparent);
}

.logo-spinner-overlay .spinner-logo {
  width: 56px;
  height: 56px;
  animation: portal-spin 1.4s linear infinite;
}

.logo-spinner-overlay .spinner-text {
  color: var(--text-secondary, #5a6b82);
  font-size: 0.9rem;
}

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

/* Respect a reduced-motion preference: keep the mark, drop the rotation. */
@media (prefers-reduced-motion: reduce) {
  .logo-spinner-overlay .spinner-logo {
    animation: none;
  }
}

/* ===== Themed modal =====
   Native alert()/confirm()/prompt() are never used: they render the host
   name in the dialog and cannot be themed. */

.portal-modal {
  position: fixed;
  inset: 0;
  z-index: 1090;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(16, 37, 71, 0.45);
}

.portal-modal.show {
  display: flex;
}

.portal-modal .portal-modal-card {
  background: var(--bg-white, #fff);
  border-radius: var(--radius-lg, 12px);
  box-shadow: var(--shadow-lg, 0 12px 32px rgba(16, 37, 71, 0.2));
  max-width: 32rem;
  width: calc(100% - 2rem);
  padding: 1.5rem;
}

/* ===== Eyebrow label =====
   All-caps, wide-tracked. The brand's standard label motif, used above
   tables and section headings. */

.eyebrow {
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  font-weight: var(--fw-semibold, 600);
  font-size: var(--fs-xs, 0.75rem);
  color: var(--text-secondary, #5a6b82);
  font-family: var(--font-brand, "Poppins", sans-serif);
}

/* ===== Data table =====
   The brand's presentation table is an alternating-navy block. Kept in the
   *header* - Deep Core ground, all-caps wide-tracked labels, the Data Stream
   accent rule beneath - because that is the part which reads as ours. The
   body follows the theme instead of being navy in both.

   Navy rows inside a white card read as a foreign object dropped onto the
   page, and on a light theme they invert the whole reading order: the table
   ends up the darkest thing on screen while carrying the least important
   text. The tokens below are already defined for both themes, so a dark-theme
   page still gets a dark table - it just gets it from the theme rather than
   from a hardcoded ramp.

   Square corners throughout: the brand leans angular, so the restrained-radii
   rule means no rounding here at all. */

.yesit-table-wrap {
  overflow-x: auto;
}

.yesit-table {
  width: 100%;
  border-collapse: collapse;
  border-radius: 0;
  background: var(--surface-card, #fff);
  color: var(--text-body, #50535a);
  margin: 0;
}

.yesit-table thead th {
  background: var(--yesit-deep-core, #102547);
  color: #fff;
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-weight: var(--fw-semibold, 600);
  font-size: var(--fs-xs, 0.75rem);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
  white-space: nowrap;
  /* The signature Data Stream rule separating head from body. */
  border-bottom: 2px solid var(--yesit-data-stream, #53c6d6);
}

/* Alternation from the theme's own two surfaces, so the stripe survives a
   theme switch without a second set of colours to keep in step. */
.yesit-table tbody tr:nth-child(odd) {
  background: var(--surface-card, #fff);
}

.yesit-table tbody tr:nth-child(even) {
  background: var(--surface-subtle, #f4f6f8);
}

.yesit-table tbody td {
  padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
  vertical-align: middle;
  border-left: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.12));
  border-bottom: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.12));
}

.yesit-table tbody tr:last-child td {
  border-bottom: 0;
}

.yesit-table tbody td:first-child {
  border-left: 0;
  font-weight: var(--fw-semibold, 600);
  color: var(--text-strong, #102547);
}

/* Secondary detail under a cell's main value, e.g. a service reference. */
.yesit-table .cell-sub {
  display: block;
  font-size: 0.8125rem;
  font-weight: var(--fw-light, 300);
  color: var(--text-muted, #7c828c);
}

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

.yesit-table .center {
  text-align: center;
}

/* A row that links to a detail page. Hover from the theme's muted surface so
   it reads as a target in either theme. */
.yesit-table--linked tbody tr:hover {
  background: var(--surface-muted, #e6ebef);
  cursor: pointer;
}

/* Terminal empty state. Plain text is correct here - the branded spinner is
   for loading, never for "there is nothing". */
.yesit-empty {
  padding: var(--space-7, 3rem) var(--space-4, 1rem);
  text-align: center;
  color: var(--text-secondary, #5a6b82);
}

/* ===== Theme-aware surfaces =====
   Bootstrap 5.2 compiles `.card`'s background to a literal `#fff` rather than
   a var reference, so the token layer's dark-theme surfaces never reach it.
   Left alone, a dark-theme page renders a white card while the text colours
   flip to light - white on white.

   The internal app patches this in its own large theme stylesheet, which the
   portal deliberately does not load, so the portal patches the handful of
   components it actually uses. `--surface-card`, `--text-strong` and
   `--text-body` are already defined for both themes in the token layer; these
   rules just point Bootstrap at them. */

.card {
  --bs-card-bg: var(--surface-card, #fff);
  --bs-card-color: var(--text-body, #50535a);
  --bs-card-border-color: var(--border-subtle, rgba(16, 37, 71, 0.12));
  color: var(--text-body, #50535a);
}

/* Headings and the eyebrow inside a card must follow the card's surface, not
   the page's - this is the pair that read as invisible before. */
.card h1,
.card h2,
.card h3,
.card h4,
.card h5,
.card h6,
.card strong {
  color: var(--text-strong, #102547);
}

.card .eyebrow {
  color: var(--text-muted, #5a6b82);
}

/* Bootstrap's .text-secondary also compiles to a fixed grey, which is too dark
   to read on the dark surface. */
.card .text-secondary,
.text-secondary {
  color: var(--text-muted, #5a6b82) !important;
}

.card hr {
  border-top-color: var(--border-default, rgba(16, 37, 71, 0.2));
  opacity: 1;
}

/* :not(.btn) matters: an <a class="btn btn-primary"> would otherwise take the
   link colour and render teal text on the teal button. */
.card a:not(.btn) {
  color: var(--text-link, #14678b);
}

/* Outline buttons inherit currentColor for their border, so they need the
   same treatment to stay visible on a dark card. */
.card .btn-outline-primary {
  --bs-btn-color: var(--text-link, #14678b);
  --bs-btn-border-color: var(--text-link, #14678b);
  --bs-btn-hover-color: var(--yesit-deep-core, #102547);
  --bs-btn-hover-bg: var(--text-link, #14678b);
  --bs-btn-hover-border-color: var(--text-link, #14678b);
}

/* ===== Tech-triangle motif =====
   The angular parallelogram outlines the website uses to break up its
   sections. Same asset the internal app's navbar uses, copied into the
   portal's own static tree (it publishes nothing outside it).

   Two things make this read like the website rather than like wallpaper:

   - It goes on a ::before rather than the element itself, because the motif
     needs to be *faint* and there is no way to fade a background-image on its
     own - `opacity` on the element would fade the content sitting on it too.
   - The teal (Data Stream) variant is used on light grounds as well as dark.
     The navy variant at full strength swamps a light section; the website
     uses teal outlines on its light bands for exactly that reason.

   Applied as a background image rather than markup so it never reaches the
   accessibility tree - it is decoration, and a screen reader announcing six
   stray paths would be worse than useless. */

.yesit-motif,
.yesit-band {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.yesit-motif::before,
.yesit-band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("../images/patterns/polygons-datastream.68b9035dd6c7.svg");
  background-repeat: no-repeat;
  background-position: left -160px center;
  background-size: auto 170%;
  opacity: 0.22;
}

/* Mirrored variant, for the right edge - the site alternates sides down the
   page rather than repeating one orientation. */
.yesit-motif--right::before,
.yesit-band--right::before {
  background-position: right -160px center;
}

/* A full-width light band carrying the motif, as the website's content
   sections do. */
.yesit-band {
  background-color: var(--surface-subtle, #f4f6f8);
  padding-top: var(--space-5, 1.5rem);
  padding-bottom: var(--space-6, 2rem);
}

html[data-theme="dark"] .yesit-band {
  background-color: var(--surface-subtle, #0d2143);
}

/* On the near-black dark surfaces the same teal reads brighter, so pull it
   back further. */
html[data-theme="dark"] .yesit-motif::before,
html[data-theme="dark"] .yesit-band::before {
  opacity: 0.16;
}

/* The motif must never come between the reader and the content. */
.yesit-motif > .container,
.yesit-band > .container {
  position: relative;
}

/* ===== Licence holders =====
   A compact trigger in the table cell, and the list itself in a modal where
   it has room and can be searched. Inline was cramped: a single subscription
   can hold dozens of names, and the seat counts are the headline. */

.seat-trigger {
  display: inline-block;
  margin-top: 0.3rem;
  padding: 0.1rem 0.5rem;
  /* Theme surfaces, not translucent white. This sits in a table cell, and
     the cell now follows the theme - a white-on-white fill with cyan text
     left the trigger invisible on a light page. */
  background: var(--surface-muted, #e6ebef);
  border: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.12));
  border-radius: var(--radius-sm, 4px);
  color: var(--text-link, #14678b);
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: 0.75rem;
  font-weight: var(--fw-medium, 500);
  cursor: pointer;
}

.seat-trigger:hover,
.seat-trigger:focus-visible {
  border-color: var(--accent, #53c6d6);
  color: var(--text-strong, #102547);
}

.seat-modal {
  display: flex;
  flex-direction: column;
  max-height: min(80vh, 40rem);
}

.seat-modal-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4, 1rem);
  margin-bottom: var(--space-4, 1rem);
}

.seat-modal-close {
  background: none;
  border: 0;
  font-size: 1.75rem;
  line-height: 1;
  padding: 0 0.25rem;
  color: var(--text-muted, #5a6b82);
  cursor: pointer;
}

.seat-modal-close:hover,
.seat-modal-close:focus-visible {
  color: var(--text-strong, #102547);
}

.seat-search {
  margin-bottom: var(--space-3, 0.75rem);
  flex: 0 0 auto;
}

/* Two columns once there is room - a long single column means a lot of
   scrolling for something people scan rather than read. */
.seat-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  flex: 1 1 auto;
  columns: 1;
  column-gap: var(--space-5, 1.5rem);
}

@media (min-width: 576px) {
  .seat-list {
    columns: 2;
  }
}

.seat-list li {
  break-inside: avoid;
  padding: 0.3rem 0;
  border-bottom: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.1));
}

.seat-name {
  display: block;
  color: var(--text-strong, #102547);
  font-weight: var(--fw-medium, 500);
  font-size: 0.9rem;
}

.seat-email {
  display: block;
  color: var(--text-muted, #5a6b82);
  font-size: 0.8125rem;
  word-break: break-all;
}

.seat-empty {
  margin: var(--space-4, 1rem) 0 0;
  color: var(--text-muted, #5a6b82);
  font-size: 0.875rem;
}

/* ===== NBN identifier cell =====
   LOC and AVC ids are long, hyphen-free strings that a customer will read out
   over the phone or copy into a form, so they get tabular figures and are kept
   from wrapping mid-identifier. */

.yesit-table .nbn-ids {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  font-size: 0.8125rem;
}

.yesit-table .id-label {
  display: inline-block;
  min-width: 2.4rem;
  color: var(--text-muted, #7c828c);
  font-weight: var(--fw-semibold, 600);
  font-size: 0.6875rem;
  letter-spacing: var(--ls-label, 0.18em);
  text-transform: uppercase;
}

/* ===== Customer-run checks =====
   The options in the "run a check" modal, and the results list beneath the
   services table. */

.check-options {
  display: grid;
  gap: var(--space-3, 0.75rem);
}

.check-option {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
  background: var(--surface-subtle, #f4f6f8);
  border: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.12));
  border-radius: var(--radius-md, 8px);
  cursor: pointer;
}

.check-option:hover:not(:disabled),
.check-option:focus-visible {
  border-color: var(--accent, #53c6d6);
  background: var(--surface-muted, #e6ebef);
}

.check-option:disabled {
  opacity: 0.6;
  cursor: progress;
}

.check-option-title {
  display: block;
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-weight: var(--fw-semibold, 600);
  color: var(--text-strong, #102547);
}

.check-option-blurb {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.8125rem;
  color: var(--text-muted, #5a6b82);
}

.check-error {
  margin: var(--space-3, 0.75rem) 0 0;
  color: var(--danger-text, #b23b34);
  font-size: 0.875rem;
}

/* ===== Starlink dish events =====
   Modelled on the events list in Starlink's app: time on the left, what
   happened in the middle, a duration chip on the right coloured by how much
   it mattered. The neutral default is for any severity the internal side
   adds later, so an unknown one never borrows the alarm colour. */

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

.event-list li {
  display: flex;
  align-items: baseline;
  gap: var(--space-3, 0.75rem);
  flex-wrap: wrap;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.1));
}

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

.event-when {
  flex: 0 0 7.5rem;
  color: var(--text-muted, #5a6b82);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

.event-what {
  flex: 1 1 12rem;
  min-width: 0;
}

.event-label {
  display: block;
  color: var(--text-body, #50535a);
}

.event-detail {
  display: block;
  color: var(--text-muted, #5a6b82);
  font-size: 0.8125rem;
}

.event-duration {
  flex: 0 0 auto;
  padding: 0.1rem 0.5rem;
  border: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.2));
  border-radius: var(--radius-sm, 4px);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted, #5a6b82);
}

.event--info .event-duration {
  border-color: var(--status-info, #2b7bb9);
}

.event--warning .event-duration {
  border-color: var(--warning-border, #d9a134);
  color: var(--warning-text, #856404);
}

.event--critical .event-duration {
  border-color: var(--danger-border, #b23b34);
  color: var(--danger-text, #b23b34);
}

/* ===== Check results ===== */

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

.check-list li {
  display: flex;
  align-items: baseline;
  gap: var(--space-3, 0.75rem);
  flex-wrap: wrap;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.1));
}

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

/* A state chip, coloured by outcome. The neutral default matters: an
   unrecognised state should read as "in progress", not as an error. */
.check-state {
  flex: 0 0 auto;
  min-width: 6rem;
  text-align: center;
  padding: 0.1rem 0.5rem;
  border-radius: var(--radius-sm, 4px);
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: 0.6875rem;
  font-weight: var(--fw-semibold, 600);
  letter-spacing: var(--ls-label, 0.18em);
  text-transform: uppercase;
  background: var(--surface-muted, #e6ebef);
  color: var(--text-muted, #5a6b82);
}

.check-state--completed {
  background: var(--success-bg, #e6f4ec);
  color: var(--success-text, #1f7a4d);
}

.check-state--failed,
.check-state--rejected {
  background: var(--danger-bg, #fdecea);
  color: var(--danger-text, #b23b34);
}

.check-outcome {
  flex: 1 1 14rem;
  color: var(--text-body, #50535a);
  font-size: 0.9rem;
}

.check-when {
  flex: 0 0 auto;
  color: var(--text-muted, #5a6b82);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}

/* ===== Linked table rows =====
   The list page is a way in to a service, not the destination. The anchor in
   the first cell stays the real focus target; the row-level affordance is for
   the mouse.

   Every colour here comes from the theme. These rules were written when the
   table body was navy in both themes, so the link was `#fff` and the chevron
   was pale cyan - which on a light page meant the *primary* value in each row
   was white text on white, legible only on the striped rows and only just.
   The row's own text and its link have to move together with the surface. */

.yesit-table--linked tbody tr {
  cursor: pointer;
}

.yesit-table a.service-link {
  color: var(--text-strong, #102547);
  font-weight: var(--fw-semibold, 600);
  text-decoration: none;
}

.yesit-table--linked tbody tr:hover a.service-link {
  color: var(--text-link, #14678b);
  text-decoration: underline;
}

/* The affordance, not a decoration: it wants to be visible but quieter than
   the value it points at. */
.yesit-table .row-chevron {
  color: var(--text-muted, #7c828c);
  font-size: 1.25rem;
  line-height: 1;
  width: 1.5rem;
}

.yesit-table--linked tbody tr:hover .row-chevron {
  color: var(--text-link, #14678b);
}

/* ===== Service detail ===== */

.back-link {
  color: var(--text-link, #14678b);
  text-decoration: none;
  font-size: 0.875rem;
}

.back-link:hover {
  text-decoration: underline;
}

/* Two columns on anything but a phone: label and value side by side reads
   faster than a stacked list when the values are short identifiers. */
.service-detail {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.15rem 1rem;
  margin: 0;
}

@media (min-width: 480px) {
  .service-detail {
    grid-template-columns: 10rem 1fr;
  }
}

.service-detail dt {
  color: var(--text-muted, #5a6b82);
  font-weight: var(--fw-regular, 400);
  font-size: 0.875rem;
}

.service-detail dd {
  margin: 0 0 0.5rem;
  color: var(--text-strong, #102547);
}

/* Identifiers the customer copies into forms or reads out over the phone:
   LOC and AVC ids, the PPPoE username, the address on the link.
   `<code>` on its own inherits the *browser's* default monospace, which on
   Windows is Courier New - dated next to Poppins, and narrow enough that a
   fifteen-digit id is hard to read back. The token layer defines --font-mono
   for exactly this and nothing was pointing at it. */
.portal-id,
.service-detail dd code {
  font-family: var(--font-mono);
  font-size: 0.9375rem;
  /* Fixed-width digits so a column of ids lines up, and a touch of tracking
     because these get read aloud one character at a time. */
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: var(--text-strong, #102547);
  background: var(--surface-subtle, #f4f6f8);
  padding: 0.05rem 0.35rem;
  border-radius: var(--radius-sm, 4px);
  /* One click selects the whole id rather than a word of it. */
  user-select: all;
}

/* ===== Network groups =====
   One section per network on the services list. Spaced enough that the
   headings read as separators without needing rules between them. */

.network-group + .network-group {
  margin-top: var(--space-6, 2rem);
}

.network-group h2 {
  color: var(--text-strong, #102547);
}

/* A quiet qualifier beside a detail value - "out of contract" and the like.
   Muted on purpose: it is context, not a warning. */
.service-detail .detail-note {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm, 4px);
  background: var(--surface-muted, #e6ebef);
  color: var(--text-muted, #5a6b82);
  font-size: 0.75rem;
}

/* ===== Connection history =====
   Three figures across the top of the panel - online now, this session, this
   month - then the session table. Deliberately plain: the numbers carry the
   meaning, so nothing here competes with them. */

.stat {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  height: 100%;
  padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
  background: var(--surface-subtle, #f4f6f8);
  border-left: 3px solid var(--yesit-data-stream, #53c6d6);
}

.stat-label {
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: var(--fs-xs, 0.75rem);
  font-weight: var(--fw-semibold, 600);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  color: var(--text-muted, #5a6b82);
}

.stat-value {
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: 1.25rem;
  font-weight: var(--fw-semibold, 600);
  color: var(--text-strong, #102547);
  font-variant-numeric: tabular-nums;
}

.stat-note {
  font-size: 0.8125rem;
  color: var(--text-muted, #5a6b82);
}

/* "current" beside a running session's duration. The Data Stream accent at
   low opacity, which holds up on either theme's surfaces. */
.yesit-table .session-now {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm, 4px);
  background: rgba(83, 198, 214, 0.18);
  color: var(--text-strong, #102547);
  font-size: 0.75rem;
}

/* ===== What the network reported =====
   NBN's own measurements under a finished check. Collapsed by default: the
   one-line verdict is the answer for most people, and this is for the few who
   go looking. Full width inside the flex row, so opening it pushes the next
   result down rather than squeezing the line above. */

.check-detail {
  flex: 1 0 100%;
  margin-top: 0.35rem;
  font-size: 0.8125rem;
}

.check-detail summary {
  cursor: pointer;
  color: var(--text-muted, #5a6b82);
}

.check-detail summary:hover {
  color: var(--text-strong, #102547);
}

.check-detail-group {
  margin: 0.6rem 0 0.2rem;
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: var(--fs-xs, 0.75rem);
  font-weight: var(--fw-semibold, 600);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  color: var(--text-muted, #5a6b82);
}

/* Two columns on anything wider than a phone; label above value below that.
   Tabular numerals so a column of sync rates lines up. */
.check-detail-rows {
  display: grid;
  grid-template-columns: minmax(10rem, max-content) 1fr;
  gap: 0.15rem var(--space-3, 0.75rem);
  margin: 0;
}

.check-detail-rows dt {
  font-weight: var(--fw-regular, 400);
  color: var(--text-muted, #5a6b82);
}

.check-detail-rows dd {
  margin: 0;
  color: var(--text-strong, #102547);
  font-variant-numeric: tabular-nums;
}

@media (max-width: 575.98px) {
  .check-detail-rows {
    grid-template-columns: 1fr;
  }

  .check-detail-rows dd {
    margin-bottom: 0.35rem;
  }
}


/* ===== Invoices =====
   A "Paid" marker and a download link, both inside the themed table. */

.invoice-paid {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: var(--radius-sm, 4px);
  background: var(--success-bg, #e6f4ec);
  color: var(--success-text, #1f7a4d);
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: 0.6875rem;
  font-weight: var(--fw-semibold, 600);
  letter-spacing: var(--ls-label, 0.18em);
  text-transform: uppercase;
}

.invoice-download {
  color: var(--text-link, #14678b);
  font-weight: var(--fw-semibold, 600);
  text-decoration: none;
  white-space: nowrap;
}

.invoice-download:hover,
.invoice-download:focus-visible {
  text-decoration: underline;
}

/* ===== Tickets =====
   A thread and a reply box. Deliberately plain: the words are the content. */

/* Newest first, down a timeline rail. Each update hangs off a dot on the
   rail; customer replies and ours take different colours. */
.ticket-timeline {
  --timeline-rail: 2px;
  --timeline-gutter: 1.75rem;
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0 0 0 var(--timeline-gutter);
}

.ticket-timeline::before {
  content: "";
  position: absolute;
  top: 0.5rem;
  bottom: 0.5rem;
  left: calc(0.5rem - var(--timeline-rail) / 2);
  width: var(--timeline-rail);
  background: var(--border-default, #d5dae3);
}

.ticket-note {
  position: relative;
}

.ticket-note + .ticket-note {
  margin-top: var(--space-3, 0.75rem);
}

.ticket-note-dot {
  position: absolute;
  top: 0.9rem;
  left: calc(-1 * var(--timeline-gutter));
  width: 1rem;
  height: 1rem;
  border-radius: 50%;
  border: 3px solid var(--surface-card, #fff);
  box-shadow: 0 0 0 1px var(--border-default, #d5dae3);
}

.ticket-note-card {
  padding: 0.75rem 1rem;
  border: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.12));
  border-left-width: 4px;
  border-radius: var(--radius-md, 8px);
}

/* Ours: brand blue. */
.ticket-note--team .ticket-note-dot {
  background: var(--yesit-data-stream, #53c6d6);
}

.ticket-note--team .ticket-note-card {
  border-left-color: var(--yesit-data-stream, #53c6d6);
  background: var(--blue-bg, #eef9fb);
}

/* The customer's: neutral, so the two read apart. */
.ticket-note--customer .ticket-note-dot {
  background: var(--text-strong, #102547);
}

.ticket-note--customer .ticket-note-card {
  border-left-color: var(--text-strong, #102547);
  background: var(--surface-subtle, #f5f7fa);
}

/* A resolution keeps its green whoever wrote it. */
.ticket-note--resolution .ticket-note-dot {
  background: var(--status-success, #2f9e6f);
}

.ticket-note--resolution .ticket-note-card {
  border-left-color: var(--status-success, #2f9e6f);
  background: var(--success-bg, #e3f5ec);
}

.ticket-note-by {
  font-weight: var(--fw-semibold, 600);
  color: var(--text-strong, #102547);
  margin-right: 0.4rem;
}

.ticket-note-latest {
  display: inline-block;
  margin-right: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm, 4px);
  background: var(--text-strong, #102547);
  color: var(--surface-card, #fff);
  font-size: 0.6875rem;
  font-weight: var(--fw-semibold, 600);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

@media (max-width: 575.98px) {
  .ticket-timeline {
    --timeline-gutter: 1.25rem;
  }

  .ticket-note-dot {
    width: 0.75rem;
    height: 0.75rem;
    border-width: 2px;
  }

  .ticket-note-card {
    padding: 0.6rem 0.75rem;
  }
}

.ticket-note-meta {
  margin: 0 0 0.25rem;
  font-size: 0.8125rem;
  color: var(--text-muted, #7c828c);
}

/* A resolution on a still-open ticket is the answer someone came for. */
.ticket-note-kind {
  display: inline-block;
  margin-right: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm, 4px);
  background: var(--success-bg, #e6f4ec);
  color: var(--success-text, #1f7a4d);
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: 0.6875rem;
  font-weight: var(--fw-semibold, 600);
  letter-spacing: var(--ls-label, 0.18em);
  text-transform: uppercase;
}

.ticket-note-text {
  color: var(--text-body, #50535a);
}

.ticket-note-text p:last-child {
  margin-bottom: 0;
}

/* Rendered note Markdown. Email links are long and unbroken, so let them
   wrap anywhere rather than push the card wider than the screen. */
.ticket-note-text {
  overflow-wrap: anywhere;
}

.ticket-note-text p {
  margin-bottom: 0.75rem;
}

.ticket-note-text ul,
.ticket-note-text ol {
  padding-left: 1.25rem;
  margin-bottom: 0.75rem;
}

.ticket-note-text h1,
.ticket-note-text h2,
.ticket-note-text h3,
.ticket-note-text h4 {
  font-size: 1rem;
  font-weight: var(--fw-semibold, 600);
  margin: 0.75rem 0 0.5rem;
}

.ticket-note-text blockquote {
  margin: 0 0 0.75rem;
  padding-left: 0.75rem;
  border-left: 3px solid var(--border-subtle, rgba(16, 37, 71, 0.15));
}

.ticket-note-text pre,
.ticket-note-text code {
  white-space: pre-wrap;
  font-size: 0.875em;
}

.ticket-note--resolution .ticket-note-text {
  color: var(--text-strong, #102547);
}

.ticket-form .form-label {
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: var(--fs-xs, 0.75rem);
  font-weight: var(--fw-semibold, 600);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  color: var(--text-muted, #7c828c);
}

.ticket-status {
  margin: var(--space-3, 0.75rem) 0 0;
  font-size: 0.875rem;
  color: var(--danger-text, #b23b34);
}

.ticket-status--ok {
  color: var(--success-text, #1f7a4d);
}

/* ----- Ticket list and detail chrome ----- */

/* Title on the left, the "new ticket" entry point on the right: present on
   every visit, but a button rather than a form taking half the page. */
.ticket-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3, 0.75rem);
}

.ticket-new-toggle,
.ticket-form .btn,
.ticket-filters .form-control,
.ticket-filters .form-select {
  min-height: 44px;
}

.ticket-form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2, 0.5rem);
}

/* Search and filters over the rendered rows. */
.ticket-filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--space-3, 0.75rem);
  margin-bottom: var(--space-2, 0.5rem);
}

.ticket-filters .form-label {
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: var(--fs-xs, 0.75rem);
  font-weight: var(--fw-semibold, 600);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  color: var(--text-muted, #7c828c);
  margin-bottom: 0.25rem;
}

.ticket-count {
  margin: 0 0 var(--space-2, 0.5rem);
}

/* A status badge. The status name is always the text, so the colour is a
   second signal, never the only one. The neutral default matters: a status
   name we do not recognise reads as "in progress". */
.ticket-badge {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-sm, 4px);
  border: 1px solid var(--border-default, #d5dae3);
  background: var(--surface-muted, #e6ebef);
  color: var(--text-strong, #102547);
  font-size: 0.8125rem;
  font-weight: var(--fw-semibold, 600);
  line-height: 1.4;
  white-space: nowrap;
}

.ticket-badge--new {
  background: var(--blue-bg, #e3f2fd);
  border-color: var(--blue-border, #14678b);
  color: var(--blue-text, #102547);
}

.ticket-badge--waiting {
  background: var(--warning-bg, #fff3cd);
  border-color: var(--warning-border, #d9a134);
  color: var(--warning-text, #856404);
}

.ticket-badge--resolved {
  background: var(--success-bg, #e3f5ec);
  border-color: var(--success-border, #2f9e6f);
  color: var(--success-text, #1c6b4a);
}

/* "Waiting on you" is the one column a customer acts on. */
.ticket-waiting--you {
  color: var(--warning-text, #856404);
  font-weight: var(--fw-semibold, 600);
}

.ticket-table tbody td:first-child {
  min-width: 12rem;
}

/* A phone stacks each row as a card, every value under its own label,
   rather than squeezing five columns into 390px (WCAG 1.4.10 reflow). */
@media (max-width: 767.98px) {
  .ticket-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .ticket-table,
  .ticket-table tbody,
  .ticket-table tr,
  .ticket-table td {
    display: block;
    width: 100%;
  }

  .ticket-table tbody tr {
    padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
    border-bottom: 1px solid var(--border-default, #d5dae3);
  }

  .ticket-table tbody tr[hidden] {
    display: none;
  }

  .ticket-table tbody td {
    display: flex;
    gap: var(--space-3, 0.75rem);
    padding: 0.2rem 0;
    border: 0;
  }

  .ticket-table tbody td:first-child {
    display: block;
    min-width: 0;
    padding-bottom: 0.4rem;
  }

  .ticket-table tbody td:not(:first-child)::before {
    content: attr(data-label);
    flex: 0 0 6.5rem;
    color: var(--text-muted, #7c828c);
    font-size: 0.8125rem;
  }
}

/* A failed load is not "nothing to show": it looks like a warning. */
.ticket-alert {
  padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
  border-left: 4px solid var(--danger-border, #990000);
  border-radius: var(--radius-sm, 4px);
  background: var(--danger-bg, #fbe5e5);
  color: var(--danger-text, #7a1212);
}

.ticket-alert a {
  color: inherit;
  font-weight: var(--fw-semibold, 600);
  text-decoration: underline;
}

/* Customer notes sit in from the left, ours from the right, so the two
   sides of the conversation separate by position as well as colour. */
@media (min-width: 576px) {
  .ticket-note--customer .ticket-note-card {
    margin-left: var(--space-7, 3rem);
  }

  .ticket-note--team .ticket-note-card {
    margin-right: var(--space-7, 3rem);
  }
}

/* Bootstrap 5.2 compiles form controls to a literal white, so the dark
   theme has to re-point them or a reply box stays white on navy. */
html[data-theme="dark"] .form-control,
html[data-theme="dark"] .form-select {
  background-color: var(--surface-subtle);
  border-color: var(--border-default);
  color: var(--text-strong);
}

html[data-theme="dark"] .form-control::placeholder {
  color: var(--text-muted);
}

/* The side panel on a ticket. */
.ticket-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 0.4rem var(--space-3, 0.75rem);
  margin: 0;
  font-size: 0.875rem;
}

.ticket-facts dt {
  color: var(--text-muted, #7c828c);
  font-weight: var(--fw-regular, 400);
}

.ticket-facts dd {
  margin: 0;
  color: var(--text-strong, #102547);
  overflow-wrap: anywhere;
}

@media (min-width: 992px) {
  .ticket-side {
    position: sticky;
    top: var(--space-4, 1rem);
  }
}

/* Who a note is from, in words: the card colour says the same thing. */
.ticket-note-side {
  display: inline-block;
  margin-right: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm, 4px);
  border: 1px solid currentcolor;
  font-size: 0.6875rem;
  font-weight: var(--fw-semibold, 600);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-strong, #102547);
}

/* The spinner sits in the status line while a write is in flight. */
.ticket-status .logo-spinner-overlay {
  padding: var(--space-3, 0.75rem) 0;
  align-items: flex-start;
  flex-direction: row;
}

.ticket-status .logo-spinner-overlay .spinner-logo {
  width: 28px;
  height: 28px;
}

/* ===== Data usage =====
   A bar plus the figures. The bar is for the glance ("am I near the limit?")
   and the numbers for the decision, so neither stands alone. */

.usage-bar {
  display: block;
  width: 100%;
  max-width: 10rem;
  height: 0.4rem;
  margin-bottom: 0.25rem;
  border-radius: var(--radius-sm, 4px);
  background: var(--surface-muted, #e6ebef);
  overflow: hidden;
}

.usage-bar span {
  display: block;
  height: 100%;
  /* Data Stream, the brand's accent, rather than a traffic light: being 60%
     through an allowance is not a warning. */
  background: var(--yesit-data-stream, #53c6d6);
}

/* At or past the allowance the service keeps working at lower priority, so
   this marks the fact without shouting about a fault. */
.usage-bar--full span {
  background: var(--warning-text, #b8860b);
}

.usage-figures {
  display: block;
  font-size: 0.8125rem;
  color: var(--text-muted, #7c828c);
  font-variant-numeric: tabular-nums;
}

/* ===== Agent installer =====
   The one place the portal hands a customer something to run. The command is
   the whole point of the page, so it gets the weight: a full-width mono block
   that wraps rather than scrolls, because a half-visible command pasted into
   PowerShell fails in a way nobody can diagnose over the phone. */

.installer-result {
  margin-top: var(--space-3, 0.75rem);
  padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
  background: var(--surface-subtle, #f4f6f8);
  border: 1px solid var(--border-subtle, rgba(16, 37, 71, 0.12));
  border-radius: var(--radius-md, 8px);
}

.installer-meta {
  margin: 0 0 var(--space-2, 0.5rem);
  font-size: 0.8125rem;
  color: var(--text-muted, #5a6b82);
}

.installer-command {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.875rem;
  /* Wrap, never clip: the customer has to get every character of this. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--text-strong, #102547);
  background: var(--surface-muted, #e6ebef);
  padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
  border-radius: var(--radius-sm, 4px);
  /* One click selects the whole command rather than a word of it - the same
     reasoning as .portal-id, and more important here because a partial
     selection still looks like a command. */
  user-select: all;
}

.installer-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3, 0.75rem);
  margin-top: var(--space-2, 0.5rem);
  flex-wrap: wrap;
}

.installer-copied {
  font-size: 0.8125rem;
  color: var(--success-text, #1f7a4d);
}

/* Steps for getting the command onto the machine. Numbered because the order
   matters - an elevated prompt opened after pasting is the usual failure. */
.installer-steps {
  margin: var(--space-3, 0.75rem) 0 0;
  padding-left: 1.2rem;
  font-size: 0.875rem;
  color: var(--text-body, #50535a);
}

.installer-steps li + li {
  margin-top: 0.25rem;
}

/* Two ways to install, each labelled. Neither is collapsed behind the other:
   downloading suits one machine and a non-technical person, the command
   suits a remote session or a run of them, and which is "easier" depends
   entirely on who is holding the mouse. */
.installer-option-label {
  margin: var(--space-3, 0.75rem) 0 0.15rem;
  font-family: var(--font-brand, "Poppins", sans-serif);
  font-size: var(--fs-xs, 0.75rem);
  font-weight: var(--fw-semibold, 600);
  text-transform: uppercase;
  letter-spacing: var(--ls-label, 0.18em);
  color: var(--text-muted, #7c828c);
}

.installer-option-blurb {
  margin: 0 0 var(--space-2, 0.5rem);
  font-size: 0.8125rem;
  color: var(--text-body, #50535a);
}

/* ===== OT critical alert =====
   Deliberately loud: the button pages a person at any hour, so the panel and
   the modal's warning say "emergency only" in the danger colours before the
   customer gets near it. */

.ot-alert-card {
  border: 2px solid var(--danger-border, #b23b34);
  background: var(--danger-bg, #fdecea);
  color: var(--danger-text, #7a1212);
}

.ot-alert-copy {
  flex: 1 1 20rem;
}

.ot-alert-warning {
  border-left: 4px solid var(--danger-border, #b23b34);
  background: var(--danger-bg, #fdecea);
  color: var(--danger-text, #7a1212);
  padding: 0.75rem 1rem;
  border-radius: var(--radius-sm, 4px);
  font-size: 0.875rem;
}

.ot-alert-state {
  margin: var(--space-3, 0.75rem) 0 0;
  font-weight: var(--fw-semibold, 600);
  font-size: 0.875rem;
}

.ot-alert-state--ok {
  color: var(--success-text, #1f7a4d);
}

.ot-alert-state--failed {
  color: var(--danger-text, #b23b34);
}

/* ===== OT critical alert console =====
   The staff /pageot console, adapted for the portal: the warning plate, a
   glass cover to lift, a sealed details panel and the big red button behind
   hazard stripes. Fixed dark colours in both themes - it is a piece of
   equipment, not a page - and sized to fit a modal on a phone. */

.portal-modal .portal-modal-card.ot-console-card {
  max-width: 36rem;
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  padding: 0;
  background: transparent;
  box-shadow: none;
}

.ot-console {
  --ot-hazard: repeating-linear-gradient(45deg, #f2c200 0 16px, #1a1a1a 16px 32px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  padding: 1.25rem;
  border-radius: 14px;
  background: linear-gradient(155deg, #9aa2ad 0%, #7c8490 45%, #656d78 100%);
  box-shadow:
    0 24px 48px rgba(0, 0, 0, 0.5),
    inset 0 2px 3px rgba(255, 255, 255, 0.35),
    inset 0 -8px 20px rgba(0, 0, 0, 0.35);
  font-family: "Courier New", monospace;
}

.ot-warning-label {
  width: 100%;
  background: #d9d2c0;
  border: 2px solid #333;
  border-radius: 5px;
  overflow: hidden;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
}

.ot-warning-hdr {
  background: #b58a00;
  color: #000;
  font-weight: 700;
  text-align: center;
  padding: 0.25rem;
  letter-spacing: 2px;
}

.ot-warning-body {
  padding: 0.6rem 0.85rem;
  color: #1a1a1a;
  font-size: 0.875rem;
  line-height: 1.4;
  text-align: center;
  font-weight: 700;
}

/* ---- Launch zone, button and glass cover ---- */

.ot-launch-wrap {
  position: relative;
  width: 170px;
  height: 170px;
  flex: 0 0 auto;
}

.ot-launch-zone {
  position: absolute;
  inset: 0;
  padding: 12px;
  background: var(--ot-hazard);
  border-radius: 8px;
  box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.4);
}

.ot-launch-inner {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  background: radial-gradient(circle at 50% 45%, #3a3a3a, #101010 80%);
}

.ot-bracket {
  position: absolute;
  width: 28px;
  height: 28px;
  border: 4px solid #e02020;
  z-index: 1;
}

.ot-bracket.tl { top: 8px; left: 8px; border-right: none; border-bottom: none; }
.ot-bracket.tr { top: 8px; right: 8px; border-left: none; border-bottom: none; }
.ot-bracket.bl { bottom: 8px; left: 8px; border-right: none; border-top: none; }
.ot-bracket.br { bottom: 8px; right: 8px; border-left: none; border-top: none; }

.ot-btn-base {
  width: 112px;
  height: 112px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 40%, #2a2a2a, #050505);
  box-shadow: 0 6px 10px rgba(0, 0, 0, 0.6);
}

.ot-big-button {
  position: relative;
  width: 90px;
  height: 90px;
  border: none;
  border-radius: 50%;
  cursor: not-allowed;
  background: radial-gradient(circle at 42% 32%, #ff6a5a, #d21f16 55%, #7d0c07 100%);
  box-shadow:
    0 12px 0 #5c0805,
    0 14px 20px rgba(0, 0, 0, 0.6),
    inset 0 4px 8px rgba(255, 255, 255, 0.5),
    inset 0 -8px 14px rgba(0, 0, 0, 0.5);
  filter: grayscale(0.35) brightness(0.8);
  transition: transform 0.08s ease, box-shadow 0.08s ease;
}

.ot-console.ready .ot-big-button {
  cursor: pointer;
  filter: none;
}

.ot-lock {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 28px;
  color: rgba(0, 0, 0, 0.55);
  pointer-events: none;
}

.ot-console.ready .ot-lock { display: none; }

.ot-console.ready .ot-big-button:active,
.ot-big-button.pressed {
  transform: translateY(9px);
  box-shadow:
    0 3px 0 #5c0805,
    0 5px 10px rgba(0, 0, 0, 0.6),
    inset 0 4px 8px rgba(255, 255, 255, 0.4),
    inset 0 -6px 12px rgba(0, 0, 0, 0.55);
}

.ot-big-button:focus-visible {
  outline: 3px solid #f2c200;
  outline-offset: 4px;
}

.ot-glass-lid {
  position: absolute;
  inset: -6px;
  z-index: 5;
  border: 3px solid rgba(200, 235, 255, 0.75);
  border-radius: 10px;
  background: linear-gradient(135deg,
    rgba(150, 220, 255, 0.28),
    rgba(180, 150, 255, 0.2) 50%,
    rgba(120, 255, 200, 0.22));
  box-shadow: inset 0 0 30px rgba(255, 255, 255, 0.35), 0 0 20px rgba(120, 200, 255, 0.4);
  transform-origin: top center;
  transition: transform 0.7s cubic-bezier(0.6, -0.2, 0.3, 1.4), opacity 0.7s ease;
  pointer-events: none;
}

.ot-glass-lid::before {
  content: "";
  position: absolute;
  top: 12%;
  left: 12%;
  width: 40%;
  height: 30%;
  border-radius: 40%;
  background: linear-gradient(120deg, rgba(255, 255, 255, 0.6), transparent);
  filter: blur(2px);
}

.ot-console.open .ot-glass-lid {
  transform: perspective(600px) rotateX(-115deg);
  opacity: 0.35;
}

.ot-case-click {
  position: absolute;
  inset: -6px;
  z-index: 6;
  border: 0;
  background: transparent;
  cursor: pointer;
}

.ot-case-click:focus-visible {
  outline: 3px solid #f2c200;
  outline-offset: 2px;
  border-radius: 10px;
}

.ot-console.open .ot-case-click { display: none; }

/* ---- Sealed details panel ---- */

.ot-msg-panel {
  position: relative;
  width: 100%;
  overflow: hidden;
  padding: 0.75rem 1rem;
  border: 3px solid #2c313a;
  border-radius: 8px;
  background: #14161b;
  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.8);
}

.ot-msg-panel > label:not(.ot-ack) {
  display: block;
  margin: 0 0 0.25rem;
  color: #9fd0ff;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
}

.ot-msg-panel textarea,
.ot-msg-panel input[type="tel"] {
  width: 100%;
  margin-bottom: 0.65rem;
  padding: 0.5rem 0.6rem;
  border: 2px solid rgba(57, 255, 90, 0.27);
  border-radius: 5px;
  outline: none;
  background: #061405;
  color: #b6ffc4;
  font-family: "Courier New", monospace;
  font-size: 0.9375rem;
}

.ot-msg-panel textarea { resize: vertical; min-height: 4rem; }

.ot-msg-panel textarea:focus,
.ot-msg-panel input[type="tel"]:focus {
  border-color: #39ff5a;
  box-shadow: 0 0 8px rgba(57, 255, 90, 0.4);
}

.ot-msg-panel textarea::placeholder,
.ot-msg-panel input::placeholder { color: #4f7a58; }

.ot-msg-panel :disabled { opacity: 0.5; cursor: not-allowed; }

.ot-ack {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  color: #f5d77a;
  font-size: 0.8125rem;
  line-height: 1.35;
  cursor: pointer;
}

.ot-ack input {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 1.1rem;
  margin-top: 0.1rem;
  accent-color: #d21f16;
}

.ot-msg-cover {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 1.25rem;
  border-radius: 5px;
  background: repeating-linear-gradient(45deg, #3a3f47 0 14px, #2c313a 14px 28px);
  color: #c7ccd4;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 1px;
  text-align: center;
  transition: transform 0.6s cubic-bezier(0.6, 0, 0.2, 1);
}

.ot-console.open .ot-msg-cover {
  transform: translateY(-105%);
  pointer-events: none;
}

/* ---- Status readout ---- */

.ot-status {
  width: 100%;
  padding: 0.65rem 1rem;
  border: 3px solid #2c313a;
  border-radius: 8px;
  background: #14161b;
  box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.8);
  text-align: center;
}

.ot-status-hint {
  color: #c7ccd4;
  font-size: 0.8125rem;
  letter-spacing: 1px;
}

.ot-status-state {
  margin-top: 0.35rem;
  color: #2fae40;
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: 2px;
  text-shadow: 0 0 10px rgba(87, 255, 106, 0.4);
}

.ot-console.open .ot-status-state {
  color: #c78a00;
  text-shadow: 0 0 10px rgba(255, 207, 63, 0.5);
}

.ot-console.error .ot-status-state,
.ot-console.sent .ot-status-state {
  color: #ff4b40;
  text-shadow: 0 0 12px rgba(255, 59, 48, 0.6);
}

.ot-status-error {
  margin: 0.5rem 0 0;
  color: #ff8a80;
  font-size: 0.875rem;
}

/* ---- Sent ---- */

.ot-sent {
  width: 100%;
  padding: 1rem;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-radius: 8px;
  background: radial-gradient(circle at 50% 30%, #ff5a6a 0%, #e0212f 55%, #8c0713 100%);
  color: #fff;
  text-align: center;
  animation: ot-pop-in 0.45s cubic-bezier(0.2, 1.4, 0.4, 1) both;
}

.ot-sent-title {
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: 1px;
  text-shadow: 0 3px 8px rgba(0, 0, 0, 0.45);
}

.ot-sent-body {
  margin: 0.5rem 0 0;
  font-size: 0.9375rem;
}

.ot-console.sent .ot-launch-wrap,
.ot-console.sent .ot-msg-panel { display: none; }

@keyframes ot-pop-in {
  0% { transform: scale(0.85); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

.ot-console-actions {
  display: flex;
  justify-content: flex-end;
  width: 100%;
}

@media (prefers-reduced-motion: reduce) {
  .ot-glass-lid,
  .ot-msg-cover,
  .ot-big-button,
  .ot-sent { transition: none; animation: none; }
}

/* The summary link is a row's focus target, so it gets a visible ring. */
.yesit-table a.service-link:focus-visible {
  outline: 2px solid var(--focus-ring, #53c6d6);
  outline-offset: 2px;
}

.ticket-note-meta time {
  white-space: nowrap;
}

/* Bootstrap's select arrow is a dark grey SVG, near invisible on the dark
   field. Same shape, stroked in the dark theme's --text-body (#c3cedb,
   about 10:1 on --surface-subtle). A data URI cannot read a CSS variable,
   so the colour is written out here. */
html[data-theme="dark"] .form-select {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23c3cedb' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}

/* The whole ticket row is the target, but the summary link stays the one
   real link: its ::after stretches over the row, so a click anywhere opens
   the ticket while keyboard and screen-reader users meet a single link.
   The focus ring stays on the link itself. Works for the stacked phone
   cards too, since the row is the positioned box either way. */
.ticket-table tbody tr {
  position: relative;
}

.ticket-table a.service-link::after {
  content: "";
  position: absolute;
  inset: 0;
}

.ticket-table tbody tr:focus-within {
  background: var(--surface-muted, #e6ebef);
}
