/* ==========================================================================
   Dongu Portal — design system
   ==========================================================================

   A bespoke system. There is no Bootstrap underneath any more: the previous
   stylesheet was, by its own description, a "Bootstrap bridge" — it recoloured
   Bootstrap's variables, which meant every component stayed a Bootstrap
   component wearing different paint. The parts that read most generically
   (pagination, modal chrome, the hamburger, the close button) were the ones it
   never touched at all.

   The organising idea is the release. This is not a store; it is the
   distribution desk of a software house, and every product is a living thing
   with a current version, a history and a support window. So version identity
   is typographically first-class, the way a model number is in a hardware
   catalogue.

   Sections
     1  Tokens          colour, type, space, radius, motion
     2  Reset & base
     3  Layout          shells, containers, page headers, sections
     4  Navigation      topbar, sidebar, breadcrumb, footer
     5  Controls        buttons, form fields
     6  Surfaces        cards, panels
     7  Indicators      status, chips, meta
     8  Data            tables, pagination, empty states
     9  Product         cards, hero, download, releases, gallery
    10  Overlay         dialog, toast
    11  Utility
    12  Environment     touch, motion, print
   ========================================================================== */


/* --------------------------------------------------------------------------
   1. Tokens
   --------------------------------------------------------------------------

   The neutrals are warm graphite — hue around 35 degrees, saturation low
   enough to read as paper and ink rather than as a colour. A deliberate move
   away from the blue-grey ramp the previous design took straight from a
   utility framework's defaults; that ramp is the most common tell of an
   interface nobody chose a palette for.

   Two accents, and only two. Olive carries action and currency ("this is the
   version you get"). Copper marks change — new, updated, the newest node on a
   release timeline. Everything else is semantic feedback and stays muted so it
   reads as system voice rather than decoration.

   Olive replaced the teal this system launched with. Teal is the default
   accent of every dashboard template written since 2015, and a distribution
   desk that looks like a SaaS dashboard is telling the customer the wrong
   thing about what it is. Olive sits in the same warm family as the neutrals
   instead of fighting them, and it is the one hue on the wheel that copper can
   stand next to without either of them turning muddy.

   Contrast ratios are noted inline, because the previous system did that and
   it is the reason its colours held up.
   -------------------------------------------------------------------------- */

:root,
[data-theme="light"] {
  color-scheme: light;

  /* Ground and surfaces — four layers, each a step above the last. */
  --ground: #faf9f7;
  --surface: #ffffff;
  --surface-sunken: #f3f1ed;
  --surface-inset: #ebe8e2;

  /* Ink */
  --ink: #1b1815;              /* 15.8:1 on surface */
  --ink-muted: #575049;        /*  7.6:1 — body-weight secondary text */
  --ink-faint: #7c746a;        /*  4.7:1 — labels and metadata only */

  /* Hairlines. Borders carry the hierarchy; shadow is reserved for layers
     that genuinely float. */
  --line: #e5e1d9;
  --line-strong: #d2ccc0;

  /* Accent — action and currency */
  --accent: #4d7c0f;           /*  5.0:1 on surface — AA at any size */
  --accent-hover: #3f6a0a;
  --accent-active: #34580a;    /*  8.3:1 under white — pressed, one step past hover */
  --accent-ink: #3f6a0a;       /*  6.4:1 — the value links and small text use */
  --accent-soft: #eef5e1;
  --accent-line: #cfe3a9;
  --accent-contrast: #ffffff;

  /* Signal — change, novelty */
  --signal: #a8500f;           /*  5.4:1 on surface */
  --signal-soft: #fbede1;
  --signal-line: #f0d3b8;

  /* Semantic */
  --ok: #2e6b3c;
  --ok-soft: #e8f2e9;
  --ok-line: #bcd9c2;
  --warn: #8a6108;
  --warn-soft: #faf0dc;
  --warn-line: #e6d3a6;
  --bad: #a3261f;
  --bad-soft: #fbeae8;
  --bad-line: #eec4c0;

  --focus: var(--accent);
  --shadow-overlay: 0 16px 48px -12px rgb(27 24 21 / 22%), 0 4px 12px -4px rgb(27 24 21 / 10%);
  --shadow-pop: 0 8px 24px -8px rgb(27 24 21 / 18%);

  /* The mark. Six tokens rather than one, because a penguin drawn in a single
     currentColor is a black blob at 24px — the belly and the beak are what
     make it legible at brand size. Body is graphite and beak is the copper
     signal, so the mark is built from the palette rather than sitting beside
     it, and it re-colours with the theme instead of needing a second asset. */
  --peng-body: #2f2a26;
  --peng-belly: #faf9f7;
  --peng-beak: #a8500f;
  --peng-foot: #a8500f;
  --peng-eye: #faf9f7;
  --peng-pupil: #2f2a26;
}

[data-theme="dark"] {
  color-scheme: dark;

  --ground: #14120f;
  --surface: #1c1917;
  --surface-sunken: #24211e;
  --surface-inset: #2d2926;

  --ink: #f2efea;
  --ink-muted: #bdb5ab;        /*  9.1:1 on surface */
  --ink-faint: #948c82;        /*  5.4:1 */

  --line: #2e2a26;
  --line-strong: #423c36;

  /* Accents lighten rather than invert — the hue stays the brand's, only the
     luminance moves to clear the dark ground. */
  --accent: #a3e635;           /* 11.6:1 on surface */
  --accent-hover: #bdf05f;
  --accent-active: #8ccc2a;    /*  8.7:1 over --accent-contrast */
  --accent-ink: #a3e635;
  --accent-soft: #23300f;
  --accent-line: #3f5518;
  --accent-contrast: #14200a;  /* Dark ink on a lit button: 11.2:1 */

  --signal: #e09455;
  --signal-soft: #35220f;
  --signal-line: #5a3a1c;

  --ok: #6bc182;
  --ok-soft: #16301d;
  --ok-line: #2b4f36;
  --warn: #dfa94e;
  --warn-soft: #332510;
  --warn-line: #55401c;
  --bad: #e8827a;
  --bad-soft: #351a18;
  --bad-line: #59302c;

  --focus: var(--accent);
  --shadow-overlay: 0 16px 48px -12px rgb(0 0 0 / 64%), 0 4px 12px -4px rgb(0 0 0 / 40%);
  --shadow-pop: 0 8px 24px -8px rgb(0 0 0 / 50%);

  /* The body lifts off the dark ground — a graphite penguin on a near-black
     header is a hole, not a mark. The belly stays the lightest thing in the
     glyph either way, which is what keeps the silhouette readable. */
  --peng-body: #7b7167;
  --peng-belly: #f2efea;
  --peng-beak: #e09455;
  --peng-foot: #e09455;
  --peng-eye: #f7f4ef;
  --peng-pupil: #23201c;
}

:root {
  /* Type -----------------------------------------------------------------
     Instrument Sans sets headings and product names: a contemporary grotesque
     with enough character to carry a title without shouting. IBM Plex Sans
     runs the interface — drawn for a hundred-odd languages, so its Turkish
     diacritics are properly fitted rather than bolted on. Plex Mono holds
     anything that is a value: versions, sizes, hashes, dates. */
  --font-display: "Instrument Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-text: "IBM Plex Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* A real scale. The previous one ran 12px to 22px, which is why no page
     could establish a hierarchy: the largest type on any screen was barely
     larger than a table cell. */
  --t-display-lg: 2.75rem;   /* 44 — product hero */
  --t-display: 2.125rem;     /* 34 — page hero */
  --t-h1: 1.75rem;           /* 28 */
  --t-h2: 1.3125rem;         /* 21 */
  --t-h3: 1.0625rem;         /* 17 */
  --t-body-lg: 1rem;         /* 16 */
  --t-body: 0.9375rem;       /* 15 */
  --t-sm: 0.875rem;          /* 14 */
  --t-xs: 0.8125rem;         /* 13 */
  --t-2xs: 0.75rem;          /* 12 */

  /* Weights stop at 600. 800 belongs on a poster, not in an interface. */
  --w-normal: 400;
  --w-medium: 500;
  --w-semi: 600;

  /* Space — 4px base, extended to 96 so pages can breathe. The old scale
     stopped at 32px, which is why every screen had the same flat rhythm
     regardless of what it was for. */
  --s-1: 0.125rem;  --s-2: 0.25rem;  --s-3: 0.375rem; --s-4: 0.5rem;
  --s-5: 0.75rem;   --s-6: 1rem;     --s-7: 1.25rem;  --s-8: 1.5rem;
  --s-9: 2rem;      --s-10: 2.5rem;  --s-11: 3rem;    --s-12: 4rem;
  --s-13: 5rem;     --s-14: 6rem;

  --r-xs: 4px;
  --r-sm: 6px;
  --r-md: 10px;
  --r-lg: 14px;
  --r-xl: 20px;
  --r-full: 999px;

  /* One control ladder for the whole app. Buttons, inputs and selects sitting
     next to each other at different heights is the most visible way an
     interface admits it was assembled rather than designed. */
  --h-xs: 1.75rem;   /* 28 */
  --h-sm: 2rem;      /* 32 */
  --h-md: 2.25rem;   /* 36 */
  --h-lg: 2.5rem;    /* 40 */

  --w-content: 1200px;
  --w-wide: 1400px;
  --w-prose: 68ch;

  /* The measured range across the systems worth copying is 100–160ms.
     Slower reads as lag; staggered entrances on page load read as a demo. */
  --motion-fast: 110ms;
  --motion: 150ms;
  --ease: cubic-bezier(0.32, 0.72, 0, 1);

  /* Layer ladder. Three names rather than three numbers scattered through the
     file: the raw values were 40, 60 and 100, which encode the order but not
     the reason, so the next sticky thing added to a page would have been a
     guess. The native dialog is deliberately absent — it renders in the top
     layer and outranks all of these without carrying a z-index at all. */
  --z-sticky: 40;    /* topbar */
  --z-toast: 60;     /* confirmations, over the topbar */
  --z-skip: 100;     /* the skip link outranks everything reachable by tab */
}


/* --------------------------------------------------------------------------
   2. Reset & base
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  /* 16px everywhere. The scaffold shipped 14px on phones, below the iOS
     auto-zoom threshold, so every form field zoomed the viewport. */
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchored release links land below the sticky topbar, not under it. */
  scroll-padding-top: 5rem;
}

body {
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--t-body);
  line-height: 1.55;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
}

/* Letter-spacing is set per size, never globally: tightening that helps a
   34px heading actively harms a 12px label. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-display);
  font-weight: var(--w-semi);
  line-height: 1.2;
  letter-spacing: -0.015em;
  text-wrap: balance;
  color: var(--ink);
}

p { text-wrap: pretty; }

a {
  color: var(--accent-ink);
  text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
  text-underline-offset: 0.2em;
  transition: color var(--motion-fast) var(--ease);
}

a:hover { color: var(--accent-hover); text-decoration-color: currentColor; }

img, svg { display: block; max-width: 100%; }
img { height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }

code, kbd, samp, pre {
  font-family: var(--font-mono);
  font-variant-ligatures: none;
  font-size: 0.9em;
}

hr { border: 0; border-top: 1px solid var(--line); margin: var(--s-9) 0; }

/* Values that sit in columns must not reflow as they change. */
.num { font-variant-numeric: tabular-nums; }

/* One icon size, one stroke, one grid — see Views/Icons.cs. Inconsistent
   iconography reads as carelessness faster than almost anything else. */
.icon { width: 1.125rem; height: 1.125rem; flex: none; }
.icon--sm { width: 1rem; height: 1rem; }
.icon--lg { width: 1.375rem; height: 1.375rem; }

/* One focus ring, on every surface. Bootstrap's white inner ring vanished
   against anything that was not white; this one never does. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}

::selection { background: var(--accent-soft); color: var(--accent-ink); }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: var(--s-6);
  top: var(--s-6);
  z-index: var(--z-skip);
  padding: var(--s-4) var(--s-6);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  font-weight: var(--w-medium);
  text-decoration: none;
  box-shadow: var(--shadow-overlay);
  transform: translateY(-250%);
}

.skip-link:focus { transform: translateY(0); }


/* --------------------------------------------------------------------------
   3. Layout
   -------------------------------------------------------------------------- */

.shell { flex: 1 0 auto; width: 100%; }

.wrap {
  width: 100%;
  max-width: var(--w-content);
  margin-inline: auto;
  padding-inline: var(--s-6);
}

.wrap--wide { max-width: var(--w-wide); }
.wrap--narrow { max-width: 46rem; }

.page { padding-block: var(--s-10) var(--s-13); }
.page--tight { padding-block: var(--s-8) var(--s-11); }

/* Page header. The eyebrow states location, the title states identity — they
   never carry the same word, or both are wasted. */
.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-6);
  margin-bottom: var(--s-9);
}

.page-head__text { min-width: 0; }

.page-head__eyebrow {
  font-size: var(--t-2xs);
  font-weight: var(--w-medium);
  letter-spacing: 0.02em;
  color: var(--ink-faint);
  margin-bottom: var(--s-3);
}

/* A status line that sits beside the page title rather than above the content:
   it reports rather than interrupts, so it gets the quietest container in the
   system — a hairline pill on the plain surface, no semantic colour. An alert
   band would have claimed something was wrong. */
.fleet-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-5);
  padding: var(--s-4) var(--s-6);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--t-xs);
  line-height: 1.5;
}

.fleet-pill .brand__mark { width: 1.375rem; height: 1.375rem; }

.page-title {
  font-size: var(--t-h1);
  letter-spacing: -0.02em;
}

.page-title--hero { font-size: var(--t-display); letter-spacing: -0.025em; }

.page-lede {
  margin-top: var(--s-4);
  max-width: 60ch;
  color: var(--ink-muted);
  font-size: var(--t-body-lg);
  line-height: 1.6;
}

/* Actions belong in one bar. One primary, at most three secondary; anything
   past that goes behind a menu. */
.page-head__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
}

/* Section. Every section gets a subline — a three-item section under a
   one-word heading reads as missing content. */
.section { margin-block: var(--s-11); }
.section:first-child { margin-top: 0; }

.section__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  margin-bottom: var(--s-7);
}

.section__title { font-size: var(--t-h2); letter-spacing: -0.018em; }

.section__sub {
  margin-top: var(--s-3);
  color: var(--ink-muted);
  font-size: var(--t-sm);
  max-width: 62ch;
}

.section__aside { font-size: var(--t-sm); color: var(--ink-faint); }

/* Two-column hub: primary content left, status and related records right.
   A detail page built as one long form is the main source of the "old CRUD
   screen" feeling. */
.hub {
  display: grid;
  gap: var(--s-9);
  align-items: start;
}

@media (min-width: 60rem) {
  .hub { grid-template-columns: minmax(0, 2fr) minmax(19rem, 1fr); gap: var(--s-10); }
  .hub__side { position: sticky; top: 5rem; }
}

.stack { display: flex; flex-direction: column; gap: var(--s-6); }
.stack--tight { gap: var(--s-4); }
.stack--loose { gap: var(--s-8); }

.row-flex {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
}

.prose { max-width: var(--w-prose); line-height: 1.7; color: var(--ink-muted); }
.prose > * + * { margin-top: var(--s-6); }
.prose h2 { font-size: var(--t-h2); color: var(--ink); margin-top: var(--s-10); }
.prose h3 { font-size: var(--t-h3); color: var(--ink); margin-top: var(--s-8); }
.prose strong { color: var(--ink); font-weight: var(--w-semi); }
.prose ul, .prose ol { padding-left: var(--s-7); }
.prose li + li { margin-top: var(--s-3); }


/* --------------------------------------------------------------------------
   4. Navigation
   -------------------------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--line);
}

.topbar__inner {
  display: flex;
  align-items: center;
  gap: var(--s-6);
  min-height: 3.75rem;
}

/* The mark is drawn inline, not loaded as an image file: a penguin built from
   the palette's own tokens, so it re-colours with the theme and needs no
   second asset and no asset pipeline. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-4);
  font-family: var(--font-display);
  font-size: var(--t-h3);
  font-weight: var(--w-semi);
  letter-spacing: -0.02em;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}

.brand:hover { color: var(--ink); }
.brand__mark { width: 1.625rem; height: 1.625rem; flex: none; }
.brand__sub { color: var(--ink-faint); font-weight: var(--w-normal); }

/* These override the literal fills baked into the mark. Class rules beat
   presentation attributes, so the SVG stands on its own before the stylesheet
   lands and follows the theme once it has. */
.peng-body { fill: var(--peng-body); }
.peng-belly { fill: var(--peng-belly); }
.peng-beak { fill: var(--peng-beak); }
.peng-foot { fill: var(--peng-foot); }
.peng-eye { fill: var(--peng-eye); }
.peng-pupil { fill: var(--peng-pupil); }

/* The mark at display size. It breathes — five seconds a cycle, five pixels of
   travel — because this is the one place in the portal where a little warmth is
   the point and nothing is waiting on the animation. Everywhere else motion is
   feedback; here it is the only decoration in the product. */
.brand-mark-lg {
  width: 4.75rem;
  height: 4.75rem;
  margin-inline: auto;
  animation: peng-float 5s ease-in-out infinite;
}

@keyframes peng-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-5px); }
}

.nav {
  display: flex;
  align-items: center;
  gap: var(--s-2);
}

.nav__link {
  display: inline-flex;
  align-items: center;
  gap: var(--s-4);
  height: var(--h-sm);
  padding-inline: var(--s-5);
  border-radius: var(--r-sm);
  color: var(--ink-muted);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}

.nav__link:hover { background: var(--surface-sunken); color: var(--ink); }

/* Current location is stated, not left to be inferred from the URL. */
.nav__link[aria-current="page"] { background: var(--accent-soft); color: var(--accent-ink); }

.topbar__end {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  margin-left: auto;
}

/* Which glyph shows is decided in CSS off the theme attribute, so it is
   already correct at first paint — the button never flashes the wrong
   direction while the script loads. The icon shown is the one you are
   switching *to*. */
.theme-icon { display: none; }
[data-theme="light"] .theme-icon--dark { display: contents; }
[data-theme="dark"] .theme-icon--light { display: contents; }

.topbar__toggle { display: none; }

/* Mobile navigation is a disclosure, not a framework component: a button
   flipping aria-expanded and a panel that respects it. */
@media (max-width: 47.99rem) {
  .topbar__toggle {
    display: inline-flex;
    margin-left: auto;
  }
  .topbar__nav {
    display: none;
    order: 10;
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: var(--s-2);
    padding-block: var(--s-5) var(--s-6);
    border-top: 1px solid var(--line);
  }
  .topbar__inner { flex-wrap: wrap; }
  .topbar[data-open="true"] .topbar__nav { display: flex; }
  .topbar__end { margin-left: 0; }
  .nav { flex-direction: column; align-items: stretch; }
  .nav__link { height: var(--h-lg); }
}

/* Admin runs on a sidebar and a sunken ground. The two faces share one design
   language but must not share one silhouette — that was the previous system's
   flattest failure: customer and operator saw the same container, the same
   density, the same background. */
.admin-shell {
  flex: 1 0 auto;
  display: grid;
  grid-template-columns: 15rem minmax(0, 1fr);
  background: var(--surface-sunken);
}

.sidebar {
  border-right: 1px solid var(--line);
  padding: var(--s-8) var(--s-5);
  position: sticky;
  top: 3.75rem;
  align-self: start;
  max-height: calc(100dvh - 3.75rem);
  overflow-y: auto;
}

.sidebar__group + .sidebar__group { margin-top: var(--s-8); }

.sidebar__label {
  padding-inline: var(--s-5);
  margin-bottom: var(--s-4);
  font-size: var(--t-2xs);
  font-weight: var(--w-medium);
  color: var(--ink-faint);
  letter-spacing: 0.02em;
}

.sidebar__link {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  height: var(--h-md);
  padding-inline: var(--s-5);
  border-radius: var(--r-sm);
  color: var(--ink-muted);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  text-decoration: none;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}

.sidebar__link:hover { background: var(--surface); color: var(--ink); }

.sidebar__link[aria-current="page"] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: inset 2px 0 0 var(--accent);
}

.sidebar__link svg { width: 1.0625rem; height: 1.0625rem; flex: none; opacity: 0.75; }
.sidebar__link[aria-current="page"] svg { opacity: 1; color: var(--accent); }

.admin-main { min-width: 0; padding: var(--s-9) var(--s-9) var(--s-13); }

@media (max-width: 63.99rem) {
  .admin-shell { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    position: static;
    max-height: none;
    border-right: 0;
    border-bottom: 1px solid var(--line);
    padding: var(--s-5);
  }
  .sidebar__group + .sidebar__group { margin-top: var(--s-5); }
  .sidebar__label { display: none; }
  .sidebar__nav { display: flex; flex-wrap: wrap; gap: var(--s-2); }
  .sidebar__link[aria-current="page"] { box-shadow: none; background: var(--accent-soft); color: var(--accent-ink); }
  .admin-main { padding: var(--s-8) var(--s-6) var(--s-11); }
}

.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  margin-bottom: var(--s-6);
  font-size: var(--t-xs);
  color: var(--ink-faint);
}

.crumbs a { color: var(--ink-muted); text-decoration: none; }
.crumbs a:hover { color: var(--ink); text-decoration: underline; }
.crumbs__sep { opacity: 0.5; }

.footer {
  border-top: 1px solid var(--line);
  padding-block: var(--s-9);
  margin-top: var(--s-12);
  color: var(--ink-faint);
  font-size: var(--t-xs);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
}

.footer a { color: var(--ink-muted); text-decoration: none; }
.footer a:hover { color: var(--ink); text-decoration: underline; }


/* --------------------------------------------------------------------------
   5. Controls
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  height: var(--h-md);
  padding-inline: var(--s-6);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--motion-fast) var(--ease),
              border-color var(--motion-fast) var(--ease),
              color var(--motion-fast) var(--ease),
              transform var(--motion-fast) var(--ease);
}

.btn svg { width: 1rem; height: 1rem; flex: none; }

.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

.btn--primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}
.btn--primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--accent-contrast); }

.btn--secondary {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink);
}
.btn--secondary:hover { background: var(--surface-sunken); border-color: var(--ink-faint); color: var(--ink); }

.btn--ghost { color: var(--ink-muted); }
.btn--ghost:hover { background: var(--surface-sunken); color: var(--ink); }

.btn--danger {
  background: var(--surface);
  border-color: var(--bad-line);
  color: var(--bad);
}
.btn--danger:hover { background: var(--bad-soft); border-color: var(--bad); color: var(--bad); }

.btn--ok {
  background: var(--surface);
  border-color: var(--ok-line);
  color: var(--ok);
}
.btn--ok:hover { background: var(--ok-soft); border-color: var(--ok); color: var(--ok); }

/* Pressed ------------------------------------------------------------------
   The system had hover on every variant and nothing at all on press, so a
   control acknowledged the cursor arriving but not the click landing. On a
   touchscreen — where there is no hover at all — that left the tap with no
   feedback whatsoever until the page navigated.

   A one-pixel dip and a deeper ground, no scale: scaling a button resamples
   its text for the length of the transition, and at 15px that reads as a
   focus wobble rather than a press. The dip is dropped under reduced motion,
   where the colour alone carries the signal.

   The filled button takes an explicit --accent-active per theme, the same way
   it already takes an explicit --accent-hover, because the two palettes want
   opposite directions: on paper the pressed green goes darker, but on the dark
   ground the button is already a lit surface and pressing a lit surface dims
   it. A single mix cannot express that. The tinted buttons need no such care —
   they press by taking on more of their own colour, which reads correctly
   either way. */
.btn:active { transform: translateY(1px); }

.btn--primary:active { background: var(--accent-active); border-color: var(--accent-active); }
.btn--secondary:active { background: var(--surface-inset); }
.btn--ghost:active { background: var(--surface-inset); }
.btn--danger:active { background: color-mix(in srgb, var(--bad-soft) 82%, var(--bad)); }
.btn--ok:active { background: color-mix(in srgb, var(--ok-soft) 82%, var(--ok)); }

.btn--lg { height: var(--h-lg); padding-inline: var(--s-7); font-size: var(--t-body); }
.btn--sm { height: var(--h-sm); padding-inline: var(--s-5); font-size: var(--t-xs); }
.btn--xs { height: var(--h-xs); padding-inline: var(--s-4); font-size: var(--t-2xs); }
.btn--icon { padding-inline: 0; width: var(--h-md); }
.btn--icon.btn--sm { width: var(--h-sm); }
.btn--block { width: 100%; }

.btn[aria-busy="true"] { opacity: 0.6; }

/* Forms ------------------------------------------------------------------- */

.field { display: flex; flex-direction: column; gap: var(--s-4); }

.label {
  font-size: var(--t-xs);
  font-weight: var(--w-medium);
  color: var(--ink);
}

.label--required::after {
  content: "*";
  margin-left: 0.2em;
  color: var(--bad);
}

.input, .select, .textarea {
  width: 100%;
  height: var(--h-md);
  padding-inline: var(--s-5);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--ink);
  font-size: var(--t-sm);
  transition: border-color var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
}

.textarea { height: auto; min-height: 6rem; padding-block: var(--s-4); line-height: 1.6; resize: vertical; }

.input::placeholder, .textarea::placeholder { color: var(--ink-faint); }

.input:hover, .select:hover, .textarea:hover { border-color: var(--ink-faint); }

.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.select {
  appearance: none;
  padding-right: var(--s-9);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%237c746a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6 4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-5) center;
  background-size: 1rem;
}

[data-theme="dark"] .select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23948c82' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6 4 4 4-4'/%3E%3C/svg%3E");
}

.input--sm, .select--sm { height: var(--h-sm); font-size: var(--t-xs); }

/* The field itself carries the error, not only the message below it. */
.input[aria-invalid="true"], .select[aria-invalid="true"], .textarea[aria-invalid="true"] {
  border-color: var(--bad);
}
.input[aria-invalid="true"]:focus { box-shadow: 0 0 0 3px var(--bad-soft); }

.field__help { font-size: var(--t-xs); color: var(--ink-faint); line-height: 1.5; }
.field__error { font-size: var(--t-xs); color: var(--bad); font-weight: var(--w-medium); }
.field__error:empty { display: none; }

.check { display: flex; align-items: center; gap: var(--s-4); }

.check input[type="checkbox"] {
  appearance: none;
  width: 1.0625rem;
  height: 1.0625rem;
  flex: none;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-xs);
  background: var(--surface);
  cursor: pointer;
  transition: background var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}

.check input[type="checkbox"]:checked {
  background: var(--accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m3.5 8.5 3 3 6-6.5'/%3E%3C/svg%3E") center / 0.8rem no-repeat;
  border-color: var(--accent);
}

.check label { font-size: var(--t-sm); color: var(--ink-muted); cursor: pointer; }

/* A fused input + action shell. Licence keys and one-time passwords are
   handed over, not typed, so the copy action belongs inside the field.

   It wraps, and that matters more than it looks. A licence key is one
   unbreakable run of characters, so an unwrapped row's minimum width is the key
   plus both buttons — about 330px — and that minimum propagates outwards: it
   widened the sidebar column, and with it the whole document, past a 375px
   viewport. Wrapping lets the buttons drop under the key on a phone instead of
   pushing the page sideways. The key itself never truncates either way. */
.secret {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3);
  max-width: 100%;
  padding: var(--s-3) var(--s-3) var(--s-3) var(--s-5);
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
}

.secret code {
  font-size: var(--t-xs);
  color: var(--ink);
  overflow-x: auto;
  white-space: nowrap;
  scrollbar-width: none;
}

.secret code::-webkit-scrollbar { display: none; }

.field-group {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding-right: var(--s-3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
}

.field-group:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.field-group .input {
  border: 0;
  background: transparent;
  box-shadow: none;
}
.field-group .input:focus { box-shadow: none; }

/* The copy control: label flips in CSS so the DOM never has to lie about
   what state it is in. */
.copy {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  height: var(--h-xs);
  padding-inline: var(--s-4);
  border: 1px solid transparent;
  border-radius: var(--r-xs);
  background: transparent;
  color: var(--ink-faint);
  font-size: var(--t-2xs);
  font-weight: var(--w-medium);
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}

.copy:hover { background: var(--surface); color: var(--ink); }
.copy svg { width: 0.875rem; height: 0.875rem; flex: none; }
.copy__done { display: none; }
.copy[data-copied="true"] { color: var(--ok); }
.copy[data-copied="true"] .copy__idle { display: none; }
.copy[data-copied="true"] .copy__done { display: inline; }


/* --------------------------------------------------------------------------
   6. Surfaces
   --------------------------------------------------------------------------
   No shadows here. A card, a table and a panel each get a hairline border;
   shadow is the signature of something that floats, and using it everywhere
   is what makes an interface read as a template.
   -------------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}

.card__body { padding: var(--s-8); }
.card__body--tight { padding: var(--s-6); }

.card__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-6) var(--s-8);
  border-bottom: 1px solid var(--line);
}

.card__title { font-size: var(--t-h3); letter-spacing: -0.01em; }
.card__foot { padding: var(--s-6) var(--s-8); border-top: 1px solid var(--line); background: var(--surface-sunken); border-radius: 0 0 var(--r-lg) var(--r-lg); }

.panel {
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--s-6);
}

/* A framed region for irreversible operations, kept away from everything
   else on the page. */
.danger-zone {
  border: 1px solid var(--bad-line);
  border-radius: var(--r-lg);
  background: var(--surface);
}

.danger-zone .card__head { border-bottom-color: var(--bad-line); }
.danger-zone .card__title { color: var(--bad); }


/* --------------------------------------------------------------------------
   7. Indicators
   --------------------------------------------------------------------------
   Badges are outlined, never filled: saturated pills repeated down a table
   make the screen mottle. And status is coded by shape as well as colour —
   a round dot for live, a square for stopped, a diamond for attention — so
   the meaning survives greyscale and colour blindness. That double coding is
   inherited from the previous system; it was the best-considered thing in it.
   -------------------------------------------------------------------------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  height: 1.5rem;
  padding-inline: var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--t-2xs);
  font-weight: var(--w-medium);
  line-height: 1;
  white-space: nowrap;
}

.chip svg { width: 0.8125rem; height: 0.8125rem; flex: none; opacity: 0.8; }
.chip--mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.chip--accent { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-ink); }
.chip--signal { background: var(--signal-soft); border-color: var(--signal-line); color: var(--signal); }
.chip--ok { background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok); }
.chip--warn { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.chip--bad { background: var(--bad-soft); border-color: var(--bad-line); color: var(--bad); }
.chip--sunken { background: var(--surface-sunken); }

.status {
  display: inline-flex;
  align-items: center;
  gap: var(--s-4);
  font-size: var(--t-xs);
  font-weight: var(--w-medium);
  color: var(--ink-muted);
  white-space: nowrap;
}

.status::before {
  content: "";
  width: 0.4375rem;
  height: 0.4375rem;
  flex: none;
  background: currentColor;
  border-radius: var(--r-full);
}

.status--on { color: var(--ok); }
.status--off { color: var(--ink-faint); }
.status--off::before { border-radius: 1px; }              /* square: stopped */
.status--warn { color: var(--warn); }
.status--warn::before { border-radius: 1px; rotate: 45deg; } /* diamond: attention */
.status--bad { color: var(--bad); }
.status--bad::before { border-radius: 1px; rotate: 45deg; }

/* Two more publishing states. Both are "live", so neither takes the stopped
   square or the attention diamond — the dot stays round and only the colour
   changes, because a staged release is the ordinary way to ship, not a fault. */
.status--stage { color: var(--accent); }
.status--beta { color: var(--signal); }

/* A staged release's dot is drawn as a ring: live, but not all the way round.
   The percentage is always written beside it, so this only makes the three
   states distinguishable at a glance for someone scanning a column of them. */
.status--stage::before {
  background: transparent;
  box-shadow: inset 0 0 0 2px currentColor;
}

/* A metadata row: values separated by hairline dots. Rows with no value are
   omitted entirely — a meta list full of em-dashes says nothing twice. */
.meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-3) var(--s-5);
  font-size: var(--t-xs);
  color: var(--ink-faint);
}

.meta__item { display: inline-flex; align-items: center; gap: var(--s-3); }
.meta__item svg { width: 0.875rem; height: 0.875rem; opacity: 0.7; }
.meta__item strong { color: var(--ink-muted); font-weight: var(--w-medium); }

.meta > * + *::before {
  content: "";
  display: inline-block;
  width: 3px; height: 3px;
  margin-right: var(--s-5);
  border-radius: var(--r-full);
  background: var(--line-strong);
  vertical-align: middle;
}

/* Definition list for detail panels. */
.facts { display: grid; gap: var(--s-5); }
.facts__row { display: grid; grid-template-columns: minmax(0, 8rem) minmax(0, 1fr); gap: var(--s-5); align-items: baseline; }
.facts__key { font-size: var(--t-xs); color: var(--ink-faint); }
.facts__val { font-size: var(--t-sm); color: var(--ink); min-width: 0; }


/* --------------------------------------------------------------------------
   8. Data
   -------------------------------------------------------------------------- */

.table-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}

.table-scroll { overflow-x: auto; }

.table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }

.table th {
  padding: var(--s-5) var(--s-6);
  text-align: left;
  font-size: var(--t-2xs);
  font-weight: var(--w-medium);
  color: var(--ink-faint);
  letter-spacing: 0.01em;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
  background: var(--surface-sunken);
}

.table td {
  padding: var(--s-5) var(--s-6);
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

.table tbody tr:last-child td { border-bottom: 0; }
.table tbody tr { transition: background var(--motion-fast) var(--ease); }
.table tbody tr:hover { background: var(--surface-sunken); }
.table__num { text-align: right; font-variant-numeric: tabular-nums; }
.table__end { text-align: right; }
.table__strong { font-weight: var(--w-medium); color: var(--ink); }
.row-muted td { opacity: 0.55; }

/* Filter bar sits directly above the data it filters, never behind a menu. */
.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  margin-bottom: var(--s-6);
}

.search {
  position: relative;
  flex: 1 1 16rem;
  max-width: 22rem;
}

.search svg {
  position: absolute;
  left: var(--s-5);
  top: 50%;
  translate: 0 -50%;
  width: 1rem; height: 1rem;
  color: var(--ink-faint);
  pointer-events: none;
}

.search .input { padding-left: var(--s-10); }

.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-5);
  margin-top: var(--s-6);
}

.pager__count { font-size: var(--t-xs); color: var(--ink-faint); font-variant-numeric: tabular-nums; }
.pager__list { display: flex; align-items: center; gap: var(--s-2); list-style: none; padding: 0; }

.pager__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--h-sm);
  height: var(--h-sm);
  padding-inline: var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
  transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}

.pager__link:hover { background: var(--surface-sunken); color: var(--ink); }

.pager__link[aria-current="page"] {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--accent-ink);
  font-weight: var(--w-medium);
}

/* Empty states. A 40px frame, a 20px monochrome glyph, one sentence, one
   action — no illustration, no emoji. And "nothing here yet" is never worded
   the same as "nothing matched your filter": they are different problems with
   different exits. */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--s-5);
  padding: var(--s-11) var(--s-8);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}

.empty__icon {
  display: grid;
  place-items: center;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: var(--r-md);
  background: var(--surface-sunken);
  color: var(--ink-faint);
}

.empty__icon svg { width: 1.25rem; height: 1.25rem; }
.empty__title { font-size: var(--t-h3); }
.empty__text { max-width: 44ch; color: var(--ink-muted); font-size: var(--t-sm); line-height: 1.6; }
.empty--inline { padding: var(--s-9) var(--s-6); background: transparent; border-style: dashed; }


/* --------------------------------------------------------------------------
   9. Product
   -------------------------------------------------------------------------- */

/* The icon tile. When a product has no uploaded icon it gets a monogram on a
   hue derived from its slug, so identity is stable and nothing ever renders a
   broken-image box or a grey "no picture" square. */
.icon-tile {
  display: grid;
  place-items: center;
  flex: none;
  width: 3.5rem;
  height: 3.5rem;
  border-radius: var(--r-md);
  background: var(--surface-sunken);
  border: 1px solid var(--line);
  overflow: hidden;
  font-family: var(--font-display);
  font-size: var(--t-h2);
  font-weight: var(--w-semi);
  letter-spacing: -0.02em;
  color: var(--ink);
  text-transform: none;
}

.icon-tile img { width: 100%; height: 100%; object-fit: cover; }
.icon-tile--lg { width: 5rem; height: 5rem; font-size: var(--t-display); border-radius: var(--r-lg); }
.icon-tile--sm { width: 2.25rem; height: 2.25rem; font-size: var(--t-body); border-radius: var(--r-sm); }

/* Twelve fixed hues rather than an inline custom property: the
   Content-Security-Policy allows no inline styles, and the hue is picked
   server-side from a hash of the slug, so a product keeps the same colour for
   good without anyone choosing one. */
.icon-tile--h0  { --tile-hue: 172; }
.icon-tile--h1  { --tile-hue: 200; }
.icon-tile--h2  { --tile-hue: 224; }
.icon-tile--h3  { --tile-hue: 258; }
.icon-tile--h4  { --tile-hue: 286; }
.icon-tile--h5  { --tile-hue: 322; }
.icon-tile--h6  { --tile-hue: 348; }
.icon-tile--h7  { --tile-hue: 18; }
.icon-tile--h8  { --tile-hue: 32; }
.icon-tile--h9  { --tile-hue: 48; }
.icon-tile--h10 { --tile-hue: 96; }
.icon-tile--h11 { --tile-hue: 142; }

.icon-tile[class*="icon-tile--h"] {
  background: hsl(var(--tile-hue) 38% 94%);
  border-color: hsl(var(--tile-hue) 30% 84%);
  color: hsl(var(--tile-hue) 48% 26%);
}

[data-theme="dark"] .icon-tile[class*="icon-tile--h"] {
  background: hsl(var(--tile-hue) 26% 16%);
  border-color: hsl(var(--tile-hue) 22% 27%);
  color: hsl(var(--tile-hue) 52% 72%);
}

/* Product grid. Cards are a fixed shape so rows never jump as descriptions
   vary in length. */
.product-grid {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
}

.product-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-6);
  padding: var(--s-7);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  transition: border-color var(--motion) var(--ease), transform var(--motion) var(--ease);
}

/* The whole card is the target; the download button inside it is a second,
   quieter target. A catalogue whose cards are just a name and a Download
   button is a file list — but a signed-in customer who already owns the
   product should not have to open a page to fetch it either. */
.product-card:hover { border-color: var(--line-strong); transform: translateY(-2px); }
.product-card:focus-within { border-color: var(--accent-line); }

.product-card__top { display: flex; align-items: flex-start; gap: var(--s-6); }
.product-card__text { min-width: 0; flex: 1; }

.product-card__name {
  font-family: var(--font-display);
  font-size: var(--t-h3);
  font-weight: var(--w-semi);
  letter-spacing: -0.012em;
  line-height: 1.3;
}

.product-card__name a { color: var(--ink); text-decoration: none; }
.product-card__name a::after { content: ""; position: absolute; inset: 0; }
.product-card__name a:hover { color: var(--accent-ink); }
.product-card { position: relative; }

/* Clamped to three lines, measured against Turkish: agglutinative endings
   run longer than the English the two-line convention was set against. */
.product-card__desc {
  margin-top: var(--s-4);
  font-size: var(--t-sm);
  line-height: 1.55;
  color: var(--ink-muted);
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.product-card__meta { margin-top: auto; }

.product-card__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}

/* Raised above the card's own click target so it stays independently
   clickable. */
.product-card__foot .btn { position: relative; z-index: 1; }

/* Product hero. Compact by design: a signed-in customer's first screen has
   work to do, and a full-height marketing splash would push it below the
   fold to show nothing. */
.product-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--s-8);
  padding-bottom: var(--s-9);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s-10);
}

.product-hero__text { flex: 1 1 22rem; min-width: 0; }
.product-hero__name { font-size: var(--t-display); letter-spacing: -0.025em; }

.product-hero__tagline {
  margin-top: var(--s-5);
  font-size: var(--t-body-lg);
  line-height: 1.6;
  color: var(--ink-muted);
  max-width: 56ch;
}

.product-hero__meta { margin-top: var(--s-6); }

/* Download panel. Three layers, in this order: the action, a single line
   saying exactly what it delivers, then the quiet secondary routes. Version,
   size and date live under the button — never inside it. */
.download {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: var(--s-7);
}

.download__label {
  font-size: var(--t-2xs);
  font-weight: var(--w-medium);
  color: var(--ink-faint);
  margin-bottom: var(--s-5);
}

.download__meta {
  margin-top: var(--s-5);
  font-family: var(--font-mono);
  font-size: var(--t-2xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  line-height: 1.6;
}

.download__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3) var(--s-6);
  margin-top: var(--s-6);
  padding-top: var(--s-6);
  border-top: 1px solid var(--line);
  font-size: var(--t-xs);
}

.download__links a { color: var(--ink-muted); text-decoration: none; }
.download__links a:hover { color: var(--accent-ink); text-decoration: underline; }

/* Release timeline. Newest first, a rail down the left, and every entry has a
   permanent anchor — support saying "that was fixed in 1.4.2" needs a link to
   exactly one row. */
.releases { position: relative; }

.release {
  position: relative;
  padding-left: var(--s-9);
  padding-bottom: var(--s-9);
}

.release::before {
  content: "";
  position: absolute;
  left: 0.3125rem;
  top: 1.5rem;
  bottom: 0;
  width: 1px;
  background: var(--line);
}

.release:last-child { padding-bottom: 0; }
.release:last-child::before { display: none; }

.release::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0.4375rem;
  width: 0.6875rem;
  height: 0.6875rem;
  border-radius: var(--r-full);
  background: var(--surface);
  border: 2px solid var(--line-strong);
}

.release--current::after { border-color: var(--accent); background: var(--accent); }
.release--pulled::after { border-color: var(--warn); }

.release__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-4);
  margin-bottom: var(--s-4);
}

.release__version {
  font-family: var(--font-mono);
  font-size: var(--t-h3);
  font-weight: var(--w-medium);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}

a.release__version:hover { color: var(--accent-ink); }
.release__body { color: var(--ink-muted); font-size: var(--t-sm); line-height: 1.65; white-space: pre-line; }
.release__foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-5); margin-top: var(--s-5); }

/* Screenshot gallery. Rendered only when there are screenshots — an empty
   frame makes a product look neglected, which is worse than no section. */
.gallery {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(min(22rem, 82%), 1fr);
  gap: var(--s-6);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-bottom: var(--s-5);
  scrollbar-width: thin;
}

.gallery__item {
  scroll-snap-align: start;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-sunken);
}

/* Fixed ratio so the gallery keeps one height across products and the page
   does not jump while images load. */
.gallery__item img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; }
.gallery__caption { padding: var(--s-4) var(--s-5); font-size: var(--t-xs); color: var(--ink-muted); border-top: 1px solid var(--line); }

.feature-list { display: grid; gap: var(--s-5); }

.feature-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--s-5);
  font-size: var(--t-sm);
  line-height: 1.6;
  color: var(--ink-muted);
}

.feature-list svg { width: 1rem; height: 1rem; flex: none; margin-top: 0.2rem; color: var(--accent); }

/* Install guide.
   Numbered because the order is real — the key cannot be entered before the
   installer runs — and a bulleted list of things that must happen in sequence
   is a list that will be done out of sequence. The number is a real ordinal in
   an <ol>, so it survives a screen reader and a stylesheet failure both. */
.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s-7); }

.step { display: flex; align-items: flex-start; gap: var(--s-6); }

.step__no {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  flex: none;
  border-radius: var(--r-full);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  color: var(--accent-ink);
  font-family: var(--font-mono);
  font-size: var(--t-sm);
  font-weight: var(--w-medium);
}

.step__text { min-width: 0; display: flex; flex-direction: column; gap: var(--s-3); }
.step__title { font-size: var(--t-h3); letter-spacing: -0.012em; }
.step__text p { color: var(--ink-muted); font-size: var(--t-sm); line-height: 1.65; max-width: 60ch; }

/* The one screen with a mascot on it. It sits in the page header rather than
   in the flow so it reads as the page's own voice, and it is the last thing to
   wrap on a narrow viewport — reassurance is the first thing a small screen
   can afford to drop. */
.buddy {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  flex: 0 1 22rem;
  padding: var(--s-6) var(--s-7);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-lg);
  background: var(--accent-soft);
}

.buddy .brand-mark-lg { width: 3.375rem; height: 3.375rem; margin-inline: 0; flex: none; }
.buddy p { font-size: var(--t-sm); line-height: 1.55; color: var(--accent-ink); }

/* Auth screens get their own shell: a portal's front door should look like a
   product's front door, not like a form floating in an empty container. */
.auth {
  flex: 1 0 auto;
  display: grid;
  place-items: center;
  padding: var(--s-11) var(--s-6) var(--s-13);
}

.auth__inner { width: 100%; max-width: 24rem; }
.auth__head { text-align: center; margin-bottom: var(--s-9); }
.auth__title { font-size: var(--t-h1); letter-spacing: -0.02em; margin-top: var(--s-6); }
.auth__sub { margin-top: var(--s-4); color: var(--ink-muted); font-size: var(--t-sm); }
.auth__foot { margin-top: var(--s-7); text-align: center; font-size: var(--t-xs); color: var(--ink-faint); }

/* The recovery-code way in, folded away. Present on the page rather than a link
   to somewhere else: whoever needs it is standing in front of a code field they
   cannot answer, which is the worst moment to be sent looking. */
.auth__alt { margin-top: var(--s-7); }

.auth__alt-summary {
  padding: var(--s-6) var(--s-7);
  font-size: var(--t-xs);
  font-weight: var(--w-medium);
  color: var(--ink-muted);
  cursor: pointer;
}

.auth__alt-summary:hover { color: var(--ink); }
.auth__alt[open] .auth__alt-summary { border-bottom: 1px solid var(--line); }

/* The enrolment QR. White ground under it always, in both themes: the code is
   drawn in black and a dark surface behind it inverts the quiet zone, which is
   the part scanners use to find the code at all. */
.qr {
  flex: none;
  background: #fff;
  padding: var(--s-5);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

/* Ten one-time codes, meant to be read across and copied down. */
.codes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: var(--s-4);
}

.codes__item {
  padding: var(--s-4) var(--s-5);
  background: var(--surface-inset);
  border-radius: var(--r-sm);
  font-size: var(--t-xs);
  text-align: center;
  user-select: all;             /* one click selects a whole code, not a word of it */
}


/* --------------------------------------------------------------------------
   10. Overlay
   -------------------------------------------------------------------------- */

.alert {
  display: flex;
  align-items: flex-start;
  gap: var(--s-5);
  padding: var(--s-6);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  font-size: var(--t-sm);
  line-height: 1.6;
}

.alert svg { width: 1.125rem; height: 1.125rem; flex: none; margin-top: 0.1rem; }
.alert--ok { background: var(--ok-soft); border-color: var(--ok-line); color: var(--ok); }
.alert--warn { background: var(--warn-soft); border-color: var(--warn-line); color: var(--warn); }
.alert--bad { background: var(--bad-soft); border-color: var(--bad-line); color: var(--bad); }
.alert--info { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-ink); }
.alert strong { font-weight: var(--w-semi); }

/* Native dialog: focus trapping, the backdrop and Escape all come from the
   platform, which is most of what the framework component was doing. */
.dialog {
  width: min(30rem, calc(100vw - 2rem));
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-overlay);
}

.dialog::backdrop { background: rgb(27 24 21 / 42%); backdrop-filter: blur(2px); }
[data-theme="dark"] .dialog::backdrop { background: rgb(0 0 0 / 62%); }

.dialog__body { padding: var(--s-8); }
.dialog__title { font-size: var(--t-h3); margin-bottom: var(--s-5); }
.dialog__text { color: var(--ink-muted); font-size: var(--t-sm); line-height: 1.65; }

.dialog__foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--s-4);
  padding: var(--s-6) var(--s-8);
  border-top: 1px solid var(--line);
  background: var(--surface-sunken);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
}

.toast-region {
  position: fixed;
  inset-block-end: var(--s-8);
  inset-inline: var(--s-6);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-4);
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  max-width: min(28rem, 100%);
  padding: var(--s-5) var(--s-6);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-pop);
  font-size: var(--t-sm);
  pointer-events: auto;
  animation: toast-in var(--motion) var(--ease);
}

.toast svg { width: 1.0625rem; height: 1.0625rem; flex: none; color: var(--accent); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}


/* --------------------------------------------------------------------------
   10b. Self-service, upload and reporting
   --------------------------------------------------------------------------
   Added with the company-administrator screens. Everything here reuses the
   tokens above: a seat row is a table row that had to survive a narrow side
   column, and the upload block is a card body with a progress track.
   -------------------------------------------------------------------------- */

/* One activated machine in the customer's own fleet, with the button that frees
   its seat. Wraps rather than truncates: the side column is narrow and the
   machine label is the part that identifies the row. */
.seat {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  padding-block: var(--s-4);
  border-top: 1px solid var(--line);
}

.seat:first-of-type { border-top: 0; padding-top: 0; }
.seat__text { display: flex; flex-direction: column; gap: var(--s-2); min-width: 0; flex: 1; }
.seat__name { font-size: var(--t-sm); color: var(--ink); font-weight: var(--w-medium); }

/* A short list of what happened, newest first. Not a table: three of these live
   inside cards where a table would bring its own grid and lose to the width. */
.feed { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; }

.feed__item {
  padding-block: var(--s-5);
  border-top: 1px solid var(--line);
}

.feed__item:first-child { border-top: 0; padding-top: 0; }
.feed__item:last-child { padding-bottom: 0; }
.feed__text { font-size: var(--t-sm); color: var(--ink); line-height: 1.5; }
.feed__meta { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-2); }

/* Proportional bars for the download report. The number is always rendered
   beside the bar, so a browser with no JS — which is what paints the width,
   since the CSP forbids style attributes — loses nothing but the comparison. */
.bars { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s-5); }

.bar {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(4rem, 1fr) auto;
  align-items: center;
  gap: var(--s-5);
  font-size: var(--t-xs);
}

.bar__label { color: var(--ink-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar__track { height: 0.5rem; border-radius: var(--r-full); background: var(--surface-inset); overflow: hidden; }
.bar__fill { display: block; height: 100%; width: 0; border-radius: var(--r-full); background: var(--accent); }
.bar__value { color: var(--ink); font-weight: var(--w-medium); }

@media (max-width: 47.99rem) {
  .bar { grid-template-columns: minmax(0, 1fr) auto; }
  .bar__track { grid-column: 1 / -1; }
}

/* Drop target for an installer. The file input covers the whole area at zero
   opacity, so a click anywhere opens the picker and the keyboard still lands on
   a real control — a label-only dropzone would be unreachable without a mouse. */
.dropzone {
  position: relative;
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-md);
  background: var(--surface-sunken);
  padding: var(--s-9) var(--s-6);
  text-align: center;
  transition: border-color var(--motion) var(--ease), background var(--motion) var(--ease);
}

.dropzone:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
.dropzone[data-dragging="true"] { border-color: var(--accent); background: var(--accent-soft); }
.dropzone[data-filled="true"] { border-style: solid; border-color: var(--accent-line); }

.dropzone__input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.dropzone__label {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-3);
  pointer-events: none;
  color: var(--ink-muted);
}

.dropzone__label svg { width: 1.5rem; height: 1.5rem; color: var(--accent); }
.dropzone__title { font-size: var(--t-sm); font-weight: var(--w-medium); color: var(--ink); }
.dropzone__hint { font-size: var(--t-xs); color: var(--ink-faint); }

.dropzone__file {
  margin-top: var(--s-5);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  color: var(--ink);
  word-break: break-all;
  pointer-events: none;
}

/* Real bytes, not a spinner: a 160 MB installer over a domestic uplink is a
   minute of silence otherwise, and silence is indistinguishable from a hang. */
.upload { display: flex; flex-direction: column; gap: var(--s-4); }

.upload__track {
  height: 0.5rem;
  border-radius: var(--r-full);
  background: var(--surface-inset);
  overflow: hidden;
}

.upload__fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--r-full);
  background: var(--accent);
  transition: width var(--motion) linear;
}

.upload[data-state="verifying"] .upload__fill { background: var(--signal); }
.upload[data-state="failed"] .upload__fill { background: var(--bad); }

/* Entitlement flags, as boxes ticked from the product's own catalogue. A
   fieldset because they are one question with several answers, which is what a
   screen reader needs to hear before it starts reading unlabelled checkboxes. */
.entitlements {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;                 /* a fieldset defaults to min-content, which will not shrink */
}

.entitlements__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4) var(--s-7);
  margin-top: var(--s-4);
}

/* Rollout steps. A segmented control rather than a number field: widening a
   rollout is the most frequent thing an operator does to a live release, and a
   field you have to type into and then submit is a field that gets set to 100
   because that is quicker than thinking about it. */
.rollout {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  flex-wrap: wrap;
}

.rollout__label {
  font-size: var(--t-xs);
  font-weight: var(--w-medium);
  color: var(--ink-muted);
}

.rollout__steps {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface);
}

.rollout__step {
  appearance: none;
  border: 0;
  border-inline-start: 1px solid var(--line);
  background: transparent;
  padding: var(--s-4) var(--s-6);
  font: inherit;
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-muted);
  cursor: pointer;
}

.rollout__step:first-child { border-inline-start: 0; }
.rollout__step:hover:not(:disabled) { background: var(--surface-sunken); color: var(--ink); }
.rollout__step:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* The current step is the one you cannot pick again — disabled rather than
   merely highlighted, so "which is it on" and "what can I do" are one answer. */
.rollout__step:disabled {
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: var(--w-medium);
  cursor: default;
}

.pager__gap { padding-inline: var(--s-3); color: var(--ink-faint); }


/* --------------------------------------------------------------------------
   11. Utility
   -------------------------------------------------------------------------- */

.muted { color: var(--ink-muted); }
.faint { color: var(--ink-faint); }
.small { font-size: var(--t-xs); }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.strong { font-weight: var(--w-medium); color: var(--ink); }
.bad-text { color: var(--bad); }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.grow { flex: 1; min-width: 0; }
.end { margin-left: auto; }
.nowrap { white-space: nowrap; }
.hidden { display: none !important; }

/* A fixed-width control in an inline form. A class rather than a style attribute
   because the Content-Security-Policy allows no inline styles. */
.w-count { width: 6.5rem; flex: none; }

.grid-2 { display: grid; gap: var(--s-6); }
@media (min-width: 48rem) { .grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.split { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-5); }


/* --------------------------------------------------------------------------
   12. Environment
   --------------------------------------------------------------------------
   Kept last on purpose: a media query adds no specificity, so these have to
   come after the rules they override.
   -------------------------------------------------------------------------- */

@media (max-width: 47.99rem) {
  /* Below 768px every field goes to 16px, or iOS zooms the viewport on focus. */
  .input, .select, .textarea { font-size: 1rem; height: var(--h-lg); }
  .page-title--hero, .product-hero__name { font-size: var(--t-h1); }
  .card__body { padding: var(--s-6); }
  .facts__row { grid-template-columns: minmax(0, 1fr); gap: var(--s-2); }
}

@media (pointer: coarse) {
  .btn, .nav__link, .sidebar__link, .pager__link { min-height: 2.75rem; }
  .copy { min-height: 2.25rem; }
}

/* Set by the theme toggle for one frame — see portal.js. A theme switch should be
   a cut, not every element dissolving into the new palette on its own timing. */
[data-theme-switching] *,
[data-theme-switching] *::before,
[data-theme-switching] *::after {
  transition: none !important;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .product-card:hover, .btn:active { transform: none; }
}

@media print {
  .topbar, .sidebar, .footer, .btn, .toast-region { display: none !important; }
  body { background: #fff; color: #000; }
  .card, .table-card { border-color: #ccc; }
}
