/**
 * Remidio Design System — the same tokens Remidio Connect uses, so the two
 * products read as one family. Values taken from that app's
 * styles/globals.css ("Light Mode UI Sheet for Dashboard Prototypes").
 *
 * Nunito Sans is self-hosted from the same woff2 files Connect ships, so
 * there is no external font request on a page behind sign-in.
 */

@font-face {
  font-family: "Nunito Sans";
  font-style: normal; font-weight: 400; font-display: swap;
  src: url("/fonts/NunitoSans-400.woff2") format("woff2");
}
@font-face {
  font-family: "Nunito Sans";
  font-style: normal; font-weight: 600; font-display: swap;
  src: url("/fonts/NunitoSans-600.woff2") format("woff2");
}
@font-face {
  font-family: "Nunito Sans";
  font-style: normal; font-weight: 700; font-display: swap;
  src: url("/fonts/NunitoSans-700.woff2") format("woff2");
}
@font-face {
  font-family: "Nunito Sans";
  font-style: normal; font-weight: 800; font-display: swap;
  src: url("/fonts/NunitoSans-800.woff2") format("woff2");
}

:root {
  /* Brand ramp, anchored on Connect's primary #3cb76d */
  --brand-50: #ecf8f1;
  --brand-100: #d2efde;
  --brand-500: #3cb76d;
  --brand-600: #2c9f5b;
  --brand-700: #1f8347;
  --brand-800: #186436;
  --brand: var(--brand-500);
  --brand-orange: #f7931e;

  /* Surfaces: content area #fafafa, cards #ffffff */
  --bg: #fafafa;
  --surface: #ffffff;
  --surface-2: #f2f2f2;
  --border: #e5e7eb;
  --border-strong: #d1d5db;

  /* Ink */
  --text: #4d4d4d;
  /* Connect's own dim is #6c757d, which reads 4.49:1 on the page - a
     hundredth under the line. It has to clear 4.5 on the DARKEST surface
     it ever lands on, which is the #f2f2f2 wells behind size tags and
     checksums, not the page. This clears all three: 5.33 on a card, 5.11
     on the page, 4.76 in a well. */
  --text-dim: #636c76;
  --text-faint: #666666;

  /* Interactive. Connect's primary is #3cb76d, but white on it reads
     2.57:1 and its edge 2.46:1 against the page - a button that is hard
     both to read and to find. One step down the same ramp fixes both
     (4.78:1 and 4.58:1) and still reads as the brand green. */
  --accent: #1f8347;
  --accent-hover: #186436;
  --accent-pressed: #25a65a;
  --accent-soft: var(--brand-50);
  --on-accent: #ffffff;

  /* Brand green as INK. The primary #3cb76d is a fill colour: as small text
     it reads 2.5:1 on white, so links, tabs and pills step down the same
     ramp to stay legible (4.8:1 on white, 6.6:1 on the tint). */
  --accent-text: var(--brand-700);
  --accent-on-soft: var(--brand-800);

  /* Status. Pill TEXT uses the darker ramp so it stays legible on tints -
     brand-700 on brand-50 reads 4.38:1, which is under the line for the
     11.5px the pills actually use. brand-800 on the same tint is 6.61. */
  --ok: var(--brand-800);
  --ok-soft: var(--brand-50);
  --warn: #8a5a00;
  --warn-soft: #fdf1dc;
  --danger: #cf222e;
  --danger-soft: #fdeceb;

  --radius: 0.625rem;
  --shadow: 0 1px 2px rgba(77, 77, 77, .05), 0 1px 3px rgba(77, 77, 77, .08);
}

/* Connect's own dark palette. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --brand-50: #13261b;
    --brand-100: #16301f;
    --brand-500: #5cc98a;
    --brand-600: #3cb76d;
    --brand-700: #2c9f5b;
    --brand: var(--brand-500);

    --bg: #14161a;
    --surface: #1a1d21;
    --surface-2: #24282e;
    --border: #2b3036;
    --border-strong: #3a4048;

    --text: #f2f2f2;
    --text-dim: #a6a6a6;
    --text-faint: #8c8c8c;

    /* Dark mode inverts the button: brand fill, near-black label. */
    --accent: #5cc98a;
    --accent-hover: #74d69c;
    --accent-pressed: #4bbd7c;
    --accent-soft: #13261b;
    --on-accent: #12160f;

    /* On dark the light green is already the legible one, so ink = fill. */
    --accent-text: #5cc98a;
    --accent-on-soft: #7ad6a0;

    --ok: #5cc98a;
    --ok-soft: #13261b;
    --warn: #e0a84a;
    --warn-soft: #2a2114;
    --danger: #f2837c;
    --danger-soft: #2d1817;
    --shadow: 0 1px 2px rgba(0, 0, 0, .35);
  }
}

* { box-sizing: border-box; }

/* `hidden` must win. Classes that set `display` (.btn is inline-flex) would
   otherwise override the UA rule and show elements the page has hidden. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.55 "Nunito Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

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

/* ---------- layout ---------- */

/* Wider than the old 940, so the content does not sit in a narrow column
   under a bar that runs the full width. Capped all the same: past about
   1200px a row of release cards stops being scannable and starts being a
   stretch of the eye. */
.shell { max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 2.5vw, 32px) 64px; }
.shell.wide { max-width: 1240px; }
.shell.centered {
  display: flex; align-items: center; justify-content: center;
  /* dvh keeps the card centred on iOS Safari, where 100vh includes the
     area behind the URL bar and pushes content off screen. */
  min-height: 100vh;
  min-height: 100dvh;
  /* Room above and below, so that when a phone keyboard shortens the
     viewport and the card has to scroll, it does not sit flush against
     either edge. Centring alone would leave no margin at all. */
  padding-top: 24px;
  padding-bottom: 24px;
}

header.top {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  margin-bottom: 28px;
}
/* The bar spans the window, so its contents should too. Holding them to
   the 940px content column left the logo 500px from the edge of a bar
   that clearly ran the whole way - at 2560px, 826px of empty white before
   the mark. The gutter now grows with the screen instead, from 16px on a
   phone to 32px on a desktop, and stops there. */
header.top .inner {
  max-width: none; margin: 0;
  padding: 14px clamp(16px, 2.5vw, 32px);
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
}

.brand {
  display: flex; align-items: center; gap: 11px; min-height: 26px;
  margin-right: auto; text-decoration: none; color: inherit;
  font-weight: 800; letter-spacing: -.01em;
}
.brand img.logo { height: 26px; width: auto; display: block; }
.brand img.logo.dark { display: none; }
.brand .sep { width: 1px; height: 20px; background: var(--border); }
.brand .title { font-weight: 700; white-space: nowrap; }
.brand .title em { color: var(--text-faint); font-weight: 400; font-style: normal; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand img.logo.light { display: none; }
  :root:not([data-theme="light"]) .brand img.logo.dark { display: block; }
}

/* Login / message cards lead with the logo instead of a text heading. */
.card-logo { height: 34px; width: auto; margin: 2px auto 18px; display: block; }
.card-logo.dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .card-logo.light { display: none; }
  :root:not([data-theme="light"]) .card-logo.dark { display: block; }
}

.who { color: var(--text-dim); font-size: 13px; }
.who b { color: var(--text); font-weight: 600; }

/* ---------- cards ---------- */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px;
  box-shadow: var(--shadow);
}
.card.narrow { width: 100%; max-width: 380px; text-align: center; }
.card + .card { margin-top: 16px; }

h1 { font-size: 22px; letter-spacing: -.02em; margin: 0 0 4px; }
h2 { font-size: 17px; letter-spacing: -.01em; margin: 0 0 2px; }
h3 { font-size: 13px; text-transform: uppercase; letter-spacing: .05em;
     color: var(--text-faint); margin: 0 0 10px; font-weight: 600; }
/* The column got wider; a line of prose should not. */
.sub { color: var(--text-dim); font-size: 13.5px; margin: 0 0 22px; max-width: 72ch; }

/* ---------- product cards ---------- */

.product {
  display: flex; flex-wrap: wrap; gap: 20px;
  align-items: flex-start; justify-content: space-between;
}
.product .meta { min-width: 220px; flex: 1; }
.version-line { display: flex; align-items: center; gap: 8px; margin: 8px 0 6px; }
.version { font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
.filemeta { color: var(--text-dim); font-size: 13px; }
.notes {
  margin: 12px 0 0; padding: 10px 12px; background: var(--surface-2);
  border-radius: 8px; font-size: 13.5px; white-space: pre-wrap;
  color: var(--text-dim); max-width: 46ch;
}
.hash {
  font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-faint); word-break: break-all; margin-top: 10px;
}

/* ---------- buttons ---------- */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  background: var(--accent); color: var(--on-accent); border: 1px solid transparent;
  border-radius: 8px; padding: 9px 16px; font-size: 14px; font-weight: 600;
  cursor: pointer; text-decoration: none; font-family: inherit;
  transition: background .15s ease-out;
}
.btn:hover { background: var(--accent-hover); }
.btn:active { background: var(--accent-pressed); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn.secondary {
  background: var(--surface); color: var(--text); border-color: var(--border);
}
.btn.secondary:hover { background: var(--surface-2); }
.btn.danger { background: transparent; color: var(--danger); border-color: var(--border); }
.btn.danger:hover { background: var(--danger-soft); }
.btn.small { padding: 5px 11px; font-size: 12.5px; }
.btn.block { width: 100%; }

/* ---------- auth panels ---------- */

.authform { text-align: left; }
.authswitch {
  margin: 10px 0 0; font-size: 13px; text-align: center; color: var(--text-faint);
}
/* Inline-block padding gets these over the 24px target minimum and keeps
   the two links comfortably apart from each other. */
.authswitch a {
  color: var(--accent-text); text-decoration: none;
  display: inline-block; padding: 8px 6px; min-height: 24px;
}
.authswitch a:hover { text-decoration: underline; }
.authswitch span { margin: 0 4px; }

/* ---------- forms ---------- */

label { display: block; font-size: 13px; font-weight: 600; margin: 0 0 5px; }
.field { margin-bottom: 14px; }
.hint { font-size: 12px; color: var(--text-faint); margin-top: 5px; }

input[type=text], input[type=email], input[type=password], input[type=search],
input[type=date], input[type=file], select, textarea {
  width: 100%; padding: 8px 11px; font: inherit; font-size: 14px;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--border); border-radius: 8px;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent);
}
textarea { resize: vertical; min-height: 72px; }

.row { display: flex; gap: 12px; flex-wrap: wrap; }
.row > * { flex: 1; min-width: 150px; }

/* ---------- tables ---------- */

.table-wrap { overflow-x: auto; margin: 0 -20px -20px; }
table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--border); }
th {
  font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-faint); font-weight: 600; white-space: nowrap;
  background: var(--surface-2); position: sticky; top: 0;
}
tbody tr:last-child td { border-bottom: none; }
td.mono { font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-dim); }
td.nowrap { white-space: nowrap; }
.truncate { max-width: 230px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- tabs ---------- */

/* Scrolls sideways on a narrow screen rather than widening the whole page,
   which would otherwise put the last tabs out of reach. */
.tabs {
  display: flex; gap: 2px; border-bottom: 1px solid var(--border);
  margin-bottom: 22px; overflow-x: auto; scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  flex: 0 0 auto;
  background: none; border: none; border-bottom: 2px solid transparent;
  padding: 9px 14px; font: inherit; font-size: 14px; font-weight: 600;
  color: var(--text-dim); cursor: pointer; margin-bottom: -1px;
}
.tab:hover { color: var(--text); }
.tab.active { color: var(--accent-text); border-bottom-color: var(--accent); }
.panel { display: none; }
.panel.active { display: block; }

/* ---------- pills + misc ---------- */

.pill {
  display: inline-block; padding: 2px 8px; border-radius: 999px;
  font-size: 11.5px; font-weight: 600; white-space: nowrap;
}
.pill.latest { background: var(--accent-soft); color: var(--accent-on-soft); }
.pill.ok { background: var(--ok-soft); color: var(--ok); }
.pill.warn { background: var(--warn-soft); color: var(--warn); }
.pill.bad { background: var(--danger-soft); color: var(--danger); }
.pill.muted { background: var(--surface-2); color: var(--text-faint); }

.stats { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 18px; }
.stat {
  flex: 1; min-width: 150px; background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 14px 16px;
}
.stat .n { font-size: 22px; font-weight: 800; letter-spacing: -.02em; }
.stat .l { font-size: 12px; color: var(--text-faint); margin-top: 1px; }

.meter { height: 5px; background: var(--surface-2); border-radius: 999px; overflow: hidden; margin-top: 9px; }
.meter > i { display: block; height: 100%; background: var(--ok); border-radius: 999px; }
.meter > i.warn { background: var(--warn); }
.meter > i.bad { background: var(--danger); }

.empty { text-align: center; color: var(--text-faint); padding: 40px 20px; font-size: 14px; }
.error-text { color: var(--danger); font-size: 13.5px; }
.ok-text { color: var(--ok); font-size: 13.5px; }

.msg { padding: 10px 13px; border-radius: 8px; font-size: 13.5px; margin-bottom: 14px; }
.msg.err { background: var(--danger-soft); color: var(--danger); }
.msg.ok { background: var(--ok-soft); color: var(--ok); }

.progress { height: 6px; background: var(--surface-2); border-radius: 999px; overflow: hidden; margin-top: 10px; }
.progress > i { display: block; height: 100%; width: 0; background: var(--accent); transition: width .2s ease; }

/* ---------- editions ---------- */

.editions { margin-top: 14px; border-top: 1px solid var(--border); }
.edition { padding: 10px 0; border-bottom: 1px solid var(--border); }
.edition:last-child { border-bottom: none; }
.edition-row { display: flex; align-items: center; gap: 14px; }
.edition-row .btn { flex: 0 0 auto; }
.edition-meta { flex: 1; min-width: 0; }
.edition-meta > div:first-child { display: flex; align-items: baseline; gap: 8px; }
.edition-file {
  font-size: 12px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; margin-top: 1px;
}
.edition .hash {
  margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.notes-toggle { margin-top: 2px; }
.notes-toggle summary {
  cursor: pointer; font-size: 12px; color: var(--accent-text);
  list-style: none;
  /* WCAG 2.2 target size: at least 24 CSS px for a pointer target. */
  display: inline-flex; align-items: center; min-height: 26px; padding: 2px 0;
}
.notes-toggle summary::-webkit-details-marker { display: none; }
.notes-toggle .notes {
  margin-top: 8px; max-height: 340px; overflow-y: auto;
  font-size: 12.5px; line-height: 1.5; max-width: none;
}
.history-release { padding: 12px 0; border-top: 1px solid var(--border); }
.history-release:first-child { border-top: none; }
.history-release .editions { margin-top: 0; border-top: none; }
.history-release .edition { padding: 6px 0; border-bottom: none; }
.notes-toggle .notes { max-width: none; }

/* ---------- upload file rows ---------- */

.upfile {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  padding: 10px 12px; background: var(--surface-2);
  border-radius: 8px; margin-bottom: 8px;
}
.upfile .name { flex: 1 1 200px; min-width: 0; font-size: 13px; }
.upfile .name b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.upfile select { flex: 0 0 165px; }
.upfile input[type=text] { flex: 1 1 200px; font-size: 12px; }
.upfile .progress { flex: 1 1 100%; margin-top: 2px; }

details.history { margin-top: 16px; border-top: 1px solid var(--border); padding-top: 12px; }
details.history summary {
  cursor: pointer; font-size: 13px; color: var(--text-dim); font-weight: 600;
  list-style: none;
  display: flex; align-items: center; min-height: 32px;
}
details.history summary::-webkit-details-marker { display: none; }

.toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 16px; }
.toolbar > div { flex: 0 1 auto; }
.toolbar label { margin-bottom: 4px; }
.toolbar input, .toolbar select { min-width: 140px; }

/* ---------- phones ---------- */

@media (max-width: 620px) {
  .product { flex-direction: column; }
  .product .actions { width: 100%; }
  .btn.block-mobile { width: 100%; }

  /* Reclaim vertical space in the header so builds start higher up. */
  header.top { margin-bottom: 20px; }
  header.top .inner { gap: 10px 12px; padding: 12px 16px; }
  .brand img.logo { height: 22px; }
  .who { font-size: 12.5px; width: 100%; }

  /* WCAG 2.2 target size is 24px, but thumbs want more. 8px+ apart. */
  .btn { min-height: 44px; }
  .btn.small { min-height: 38px; padding: 8px 14px; font-size: 13px; }
  .tab { min-height: 44px; padding: 10px 14px; }
  .edition .btn { min-height: 40px; }

  /* Log filters stack instead of squeezing into unusable columns. */
  .toolbar { gap: 12px; }
  .toolbar > div { flex: 1 1 100%; min-width: 0; }
  .toolbar input, .toolbar select { width: 100%; min-width: 0; }
  .toolbar > div:last-child { display: flex; gap: 8px; }
  .toolbar > div:last-child .btn { flex: 1; }

  /* Tables keep their own horizontal scroll inside the card. */
  .card { padding: 16px; }
  .table-wrap { margin: 0 -16px -16px; }

  .shell { padding-bottom: 40px; }
  .stat { min-width: 120px; }
  .notes { max-width: none; }

  /* iOS Safari zooms the page whenever a focused field is under 16px.
     16px here stops that; it is not a typographic choice. */
  input[type=text], input[type=email], input[type=password], input[type=search],
  input[type=date], input[type=file], select, textarea {
    font-size: 16px;
  }
}

/* Anyone who prefers less motion gets none of ours. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==================================================================
   Brand motion
   ------------------------------------------------------------------
   Remidio's own devices, translated for a utility page: the eye from
   remidio.com (there, a 720p looping video - here, a 2 KB drawing that
   recolours with the theme) and the butterfly from the logo mark.

   Both are deliberately rationed. The guidance is that infinite
   decorative motion is an anti-pattern and that no more than one or two
   elements should move in a view: so the eye blinks only on the page
   you see once per session, and the butterfly flies only in answer to
   something you did.
   ================================================================== */

/* A wash of brand green behind the cards that stand on their own - reset,
   set-password, the refusal page. Sign-in has the film instead. On its own
   layer rather than on the shell, whose 940px cap would clip it into a
   visible rectangle. */
.shell.centered::before {
  content: ""; position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(70rem 36rem at 50% -10rem, var(--brand-50), transparent 68%),
    radial-gradient(34rem 24rem at 95% 105%, var(--brand-50), transparent 70%);
}
body.signin .shell.centered::before { display: none; }

/* ---------- the eye (sign-in only) ---------- */

/* The film is framed with the face to the right and the left half falling
   away to near-white, which is a sign-in page already: the card takes the
   pale side, the eye keeps its own. */
.eye-stage {
  position: fixed; inset: 0; z-index: -1; overflow: hidden;
  background: var(--bg);
}
.eye-stage video {
  width: 100%; height: 100%; object-fit: cover; object-position: 66% 42%;
  display: block;
}
/* Page colour drawn across the card's side, so the form never has to sit
   on skin no matter how the film is cropped. */
.eye-stage::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(97deg,
    var(--bg) 0%, var(--bg) 24%, rgba(250, 250, 250, .72) 40%, transparent 64%);
}
body.signin .shell.centered {
  max-width: none;
  justify-content: flex-start;
  padding-left: min(9vw, 120px);
}

/* Shot for a wide frame. Cropped to a phone it becomes an area of skin, so
   the film takes a band across the top instead and the card sits under it. */
@media (max-width: 860px) {
  .eye-stage { position: absolute; bottom: auto; height: 38vh; min-height: 210px; }
  .eye-stage video { object-position: 63% 46%; }
  .eye-stage::after {
    background: linear-gradient(to bottom,
      transparent 34%, rgba(250, 250, 250, .55) 72%, var(--bg) 97%);
  }
  body.signin .shell.centered {
    display: block; justify-content: unset;
    padding-left: 16px; padding-right: 16px;
    padding-top: calc(38vh - 52px);
    min-height: auto;
  }
  body.signin .card.narrow { margin: 0 auto; }
}

/* The film is lit for a white page. On dark it would glare, so it is taken
   down to sit behind the card rather than in front of it. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .eye-stage video { filter: brightness(.5) saturate(.85); }
  :root:not([data-theme="light"]) .eye-stage::after {
    background: linear-gradient(97deg,
      var(--bg) 0%, var(--bg) 24%, rgba(20, 22, 26, .78) 42%, rgba(20, 22, 26, .35) 66%);
  }
}
@media (prefers-color-scheme: dark) and (max-width: 860px) {
  :root:not([data-theme="light"]) .eye-stage::after {
    background: linear-gradient(to bottom,
      rgba(20, 22, 26, .25) 30%, rgba(20, 22, 26, .7) 72%, var(--bg) 97%);
  }
}

/* ---------- the butterfly ---------- */

/* Three nested elements: the outer travels sideways at a constant rate,
   the inner falls away upward on an ease - together a real arc rather
   than a straight line - and the mark itself beats its wings. */
.flutter {
  position: fixed; z-index: 60;
  width: var(--bf-size, 28px); height: var(--bf-size, 28px);
  pointer-events: none; will-change: transform;
  animation: bf-drift var(--bf-dur, 1100ms) linear forwards;
}
.flutter > i { display: block; width: 100%; height: 100%;
               animation: bf-rise var(--bf-dur, 1100ms) cubic-bezier(.3, 0, .5, 1) forwards; }
.flutter img { display: block; width: 100%; height: 100%;
               animation: bf-wings 190ms ease-in-out infinite; }

@keyframes bf-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(var(--bf-x, 0px)); }
}
@keyframes bf-rise {
  0%   { transform: translateY(0) scale(.55); opacity: 0; }
  14%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { transform: translateY(var(--bf-y, -150px)) scale(1); opacity: 0; }
}
/* Wings seen flat, then closer to edge-on. Not so fast or so narrow that
   it stops reading as wings and becomes a flicker. */
@keyframes bf-wings {
  0%, 100% { transform: scaleX(1); }
  50%      { transform: scaleX(.6); }
}

/* ---------- colour that carries meaning ---------- */

/* Which edition someone is about to hand a customer is worth seeing
   before the click, not after. The name is still spelled out beside it,
   so the colour is never the only thing saying it. */
.edition { border-left: 3px solid transparent; padding-left: 11px; }
.edition[data-edition="customer-cloud"],
.edition[data-edition="customer-offline"] { border-left-color: var(--accent); }
.edition[data-edition="demo"]             { border-left-color: var(--brand-orange); }
.edition[data-edition="research"]         { border-left-color: var(--border-strong); }

/* ---------- the download button answers back ---------- */

.btn.working { opacity: .75; pointer-events: none; }

/* Nothing here is load-bearing: switched off, the page still tells you
   everything, in words. */
@media (prefers-reduced-motion: reduce) {
  .eyemark .eye { animation: none; }
  .flutter { display: none; }
  .btn, .tab, a { transition: none; }
}

/* Reachable by a screen reader, invisible on screen. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

/* ==================================================================
   Transitions and finish
   ------------------------------------------------------------------
   The rule followed throughout: decelerate on arrival, accelerate on
   departure, and let leaving be quicker than coming - which is what
   makes a handover feel like one movement rather than two. Nothing
   overshoots: bounce reads as sloppy on a page of build numbers.
   ================================================================== */

/* ---------- between pages ---------- */

/* Signing in is a handover, so the pages slide rather than blink. The
   browsers that do not have this simply navigate as before. */
@view-transition { navigation: auto; }

@keyframes page-in  { from { opacity: 0; transform: translateX(22px); } }
@keyframes page-out { to   { opacity: 0; transform: translateX(-18px); } }

::view-transition-old(root) { animation: page-out 170ms cubic-bezier(.4, 0, 1, 1) both; }
::view-transition-new(root) { animation: page-in  260ms cubic-bezier(0, 0, .2, 1) both; }

/* ---------- arriving ---------- */

@keyframes rise { from { opacity: 0; transform: translateY(10px); } }

/* The sign-in card settles in rather than appearing. */
body.signin .card.narrow { animation: rise 380ms cubic-bezier(0, 0, .2, 1) both; }

/* Releases arrive in order, a beat apart. Kept to 40ms a card and
   capped below, so a long history never turns into a queue. */
.card.arrive {
  animation: rise 300ms cubic-bezier(0, 0, .2, 1) both;
  animation-delay: calc(var(--i, 0) * 40ms);
}
.edition.arrive {
  animation: rise 260ms cubic-bezier(0, 0, .2, 1) both;
  animation-delay: calc(90ms + var(--i, 0) * 45ms);
}

/* ---------- under the hand ---------- */

.btn {
  transition: background .15s ease-out, transform .12s ease-out, box-shadow .15s ease-out;
}
.btn:hover  { transform: translateY(-1px); box-shadow: 0 2px 6px rgba(31, 131, 71, .22); }
.btn:active { transform: translateY(0) scale(.985); box-shadow: none; }
.btn.working, .btn:disabled { transform: none; box-shadow: none; }

input, select, textarea { transition: border-color .15s ease-out, box-shadow .15s ease-out; }
input:focus, select:focus, textarea:focus { box-shadow: 0 0 0 3px var(--accent-soft); }

/* Every control that takes focus shows it. Buttons and links had only
   the browser's own ring, which on a green fill is close to invisible. */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
  border-radius: 4px;
}
.btn:focus-visible { outline-offset: 3px; }

.notes-toggle summary, details.history summary { transition: color .15s ease-out; }
.notes-toggle summary:hover, details.history summary:hover { color: var(--accent-text); }
.notes-toggle > div, details.history > *:not(summary) {
  animation: rise 200ms cubic-bezier(0, 0, .2, 1) both;
}

/* ---------- while it loads ---------- */

/* Standing in for the list rather than leaving the page empty. Nothing
   sits below it, so replacing it shifts nothing. */
.skeleton { pointer-events: none; }
.sk {
  background: var(--surface-2); border-radius: 6px; height: 12px;
  animation: sk-breathe 1.4s ease-in-out infinite;
}
.sk + .sk { margin-top: 8px; }
.sk.title { height: 17px; width: 34%; }
.sk.sub   { width: 52%; }
.sk.big   { height: 26px; width: 24%; margin-top: 14px; }
.sk.row   { height: 54px; margin-top: 12px; border-radius: 8px; }
@keyframes sk-breathe { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }

/* ---------- switched off ---------- */

@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
  body.signin .card.narrow,
  .card.arrive, .edition.arrive,
  .notes-toggle > div, details.history > *:not(summary) { animation: none; }
  .sk { animation: none; }
  .btn:hover, .btn:active { transform: none; }
}

/* ==================================================================
   Structure
   ------------------------------------------------------------------
   Swiss discipline rather than ornament: one type scale, one icon
   family, generous white space, and hierarchy carried by size and
   spacing instead of decoration. What was added is an icon set and a
   grid; what was taken away is a wall of hexadecimal.
   ================================================================== */

/* ---------- icons ---------- */

.ico { flex: 0 0 auto; display: block; }
/* Beside a label, an icon reads as part of it - so they sit on one line
   with a single gap rather than being positioned case by case. */
.btn .ico, .fact .ico, .edition-file .ico,
.verify summary .ico, .history summary .ico, .copybtn .ico { margin-right: 0; }
.fact, .edition-file, .verify summary, .history summary, .copybtn {
  display: inline-flex; align-items: center; gap: 6px;
}

/* ---------- the release at a glance ---------- */

.product-head { margin-bottom: 2px; }

/* Three separate facts, each findable, rather than one grey sentence. */
.facts {
  display: flex; flex-wrap: wrap; gap: 6px 18px;
  color: var(--text-dim); font-size: 13px; margin-top: 2px;
}
.facts .ico { color: var(--text-faint); }

/* ---------- one edition ---------- */

.edition-name { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.edition-name b { font-size: 15px; }

/* The size belongs to the file, so it travels with the name rather than
   joining the queue of grey metadata underneath. */
.size-tag {
  font-size: 11.5px; font-weight: 600; color: var(--text-dim);
  background: var(--surface-2); border-radius: 999px; padding: 1px 8px;
  white-space: nowrap;
}

.edition-file { max-width: 100%; }
.edition-file span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.edition-extras {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 18px;
  margin-top: 6px;
}

/* ---------- the checksum, when it is actually wanted ---------- */

.verify summary {
  cursor: pointer; font-size: 12px; color: var(--text-dim);
  list-style: none; min-height: 28px;
}
.verify summary::-webkit-details-marker { display: none; }
.verify summary:hover { color: var(--accent-text); }
.verify[open] summary { color: var(--accent-text); }

.verify-body {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 6px 0 2px; padding: 9px 11px;
  background: var(--surface-2); border-radius: 8px;
}
.verify-body .hash {
  flex: 1 1 22ch; margin: 0; min-width: 0;
  font: 11.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text-dim); word-break: break-all; user-select: all;
}

.copybtn {
  flex: 0 0 auto; cursor: pointer; font: inherit; font-size: 12px;
  font-weight: 600; color: var(--text-dim);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 7px; padding: 5px 10px; min-height: 28px;
  transition: color .15s ease-out, border-color .15s ease-out;
}
.copybtn:hover { color: var(--accent-text); border-color: var(--border-strong); }
.copybtn.copied { color: var(--ok); border-color: var(--ok); }

/* ---------- nothing here yet ---------- */

.empty-state {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  text-align: center; padding: 30px 16px 26px; margin-top: 12px;
  border: 1px dashed var(--border-strong); border-radius: var(--radius);
  color: var(--text-dim);
}
.empty-state .ico { color: var(--text-faint); }
.empty-state .empty-title { font-weight: 700; color: var(--text); margin-top: 2px; }

/* ---------- phone ---------- */

@media (max-width: 560px) {
  /* The button stops sharing a line with a filename that has nowhere left
     to go, and spans the row instead - a bigger, surer target. */
  .edition-row { flex-wrap: wrap; gap: 10px; }
  .edition-row .btn { width: 100%; }
  .verify-body { gap: 8px; }
  .copybtn { width: 100%; justify-content: center; }
}

/* Both disclosures on a row now lead with an icon, so they read as a pair
   rather than one bulleted and one not. */
.notes-toggle summary {
  display: inline-flex; align-items: center; gap: 6px;
  list-style: none; min-height: 28px;
}
.notes-toggle summary::-webkit-details-marker { display: none; }
.notes-toggle[open] summary { color: var(--accent-text); }

/* ---------- admin: tiles ---------- */

.stat { display: flex; align-items: center; gap: 12px; }
.stat-ico {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--accent-soft); color: var(--accent-text);
}

/* ---------- admin: the upload surface ---------- */

/* A surface drawn around the real input rather than instead of it: the
   input keeps its label, its focus and its keyboard behaviour, and simply
   sits invisibly across the whole zone. */
.dropzone {
  position: relative; display: block;
  border: 1.5px dashed var(--border-strong); border-radius: var(--radius);
  background: var(--surface-2);
  transition: border-color .15s ease-out, background .15s ease-out;
}
.dropzone input[type=file] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; cursor: pointer;
}
.dropzone-face {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  padding: 26px 16px; text-align: center; pointer-events: none;
}
.dropzone-ico { color: var(--text-faint); transition: color .15s ease-out; }
.dropzone-title { font-size: 13.5px; font-weight: 600; color: var(--text); }
.dropzone-sub { font-size: 12px; color: var(--text-dim); }

.dropzone:hover { border-color: var(--accent); }
/* The input is what actually receives focus, so the ring is drawn on the
   surface around it - otherwise focus would land on an invisible element. */
.dropzone:focus-within {
  border-color: var(--accent);
  outline: 2px solid var(--accent-text); outline-offset: 2px;
}
.dropzone.over {
  border-color: var(--accent); background: var(--accent-soft);
  border-style: solid;
}
.dropzone.over .dropzone-ico, .dropzone.filled .dropzone-ico { color: var(--accent-text); }
.dropzone.filled { border-style: solid; border-color: var(--accent); background: var(--accent-soft); }

/* ---------- admin: an empty table ---------- */

.empty-inline {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 18px 0; color: var(--text-dim);
}
.empty-inline .ico { color: var(--text-faint); }

/* Open, the checksum needs the whole row - otherwise it wraps around its
   neighbour and the pair stops looking like a pair. */
.edition-extras .verify[open] { flex: 1 1 100%; }

/* ==================================================================
   Edition colour
   ------------------------------------------------------------------
   One hue, one mark and one name per edition. Every pair below was
   measured on its own tint: 5.3:1 at worst in light, 7.2:1 at worst in
   dark. The name is always written out, so the colour is the second
   thing saying which build this is, never the only one.
   ================================================================== */

:root {
  --tone-cloud:    #186436;  --tone-cloud-soft:    #ecf8f1;
  --tone-offline:  #0f6070;  --tone-offline-soft:  #e6f4f7;
  --tone-demo:     #8a5a00;  --tone-demo-soft:     #fdf1dc;
  --tone-research: #5b3ba8;  --tone-research-soft: #f1ecfb;
  --tone-plain:    #636c76;  --tone-plain-soft:    #f2f2f2;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --tone-cloud:    #7ad6a0;  --tone-cloud-soft:    #13261b;
    --tone-offline:  #6fc9dc;  --tone-offline-soft:  #102227;
    --tone-demo:     #e8b463;  --tone-demo-soft:     #2a2114;
    --tone-research: #b49bef;  --tone-research-soft: #1d1830;
    --tone-plain:    #a6a6a6;  --tone-plain-soft:    #24282e;
  }
}

.edition[data-tone="cloud"]    { --tone: var(--tone-cloud);    --tone-soft: var(--tone-cloud-soft); }
.edition[data-tone="offline"]  { --tone: var(--tone-offline);  --tone-soft: var(--tone-offline-soft); }
.edition[data-tone="demo"]     { --tone: var(--tone-demo);     --tone-soft: var(--tone-demo-soft); }
.edition[data-tone="research"] { --tone: var(--tone-research); --tone-soft: var(--tone-research-soft); }
.edition[data-tone="plain"]    { --tone: var(--tone-plain);    --tone-soft: var(--tone-plain-soft); }

/* The stripe now takes the edition's own ink rather than one of three
   greens, so the four are told apart at the edge of vision. */
.edition[data-tone] { border-left-color: var(--tone); }

/* The mark: the edition's icon on the edition's tint. */
.edition-mark {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 34px; height: 34px; border-radius: 9px;
  background: var(--tone-soft); color: var(--tone);
}
.edition-row { align-items: center; }

/* The name picks up the hue too, at full ink weight so it stays readable. */
.edition-name b { color: var(--tone); }

@media (max-width: 560px) {
  .edition-row { align-items: flex-start; }
  .edition-mark { width: 30px; height: 30px; }
}

/* ---------- the card itself ---------- */

/* A band of brand colour across the top of each product, so the page
   opens with something other than grey on white. Kept to a hairline and
   a wash: the content is what should carry weight, not the frame. */
.product-card {
  position: relative; overflow: hidden;
  padding-top: 22px;
}
.product-card::before {
  content: ""; position: absolute; inset: 0 0 auto; height: 3px;
  background: linear-gradient(90deg,
    var(--tone-cloud) 0%, var(--accent) 34%,
    var(--tone-demo) 68%, var(--tone-research) 100%);
  opacity: .9;
}
.product-card::after {
  content: ""; position: absolute; inset: 3px 0 auto; height: 96px;
  background: linear-gradient(180deg, var(--accent-soft), transparent);
  opacity: .55; pointer-events: none;
}
.product-card > * { position: relative; }

/* The version is the fact people came for, so it gets the brand colour
   and the page's largest type. */
.version { color: var(--accent-text); }

/* The heading area sits above the wash. */
.product-head h2 { letter-spacing: -.015em; }

/* ==================================================================
   Choosing a team
   ================================================================== */

.role-grid {
  display: grid; gap: 14px; margin: 22px 0 18px;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}

/* A button, not a div with a click handler: it arrives in the tab order,
   answers the space bar and announces itself, all without being told. */
.role-card {
  display: flex; flex-direction: column; gap: 6px;
  text-align: left; font: inherit; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 16px; box-shadow: var(--shadow);
  transition: border-color .15s ease-out, transform .12s ease-out, box-shadow .15s ease-out;
}
.role-card:hover {
  border-color: var(--accent); transform: translateY(-1px);
  box-shadow: 0 3px 10px rgba(31, 131, 71, .14);
}
.role-card:active { transform: translateY(0); }

.role-card-head { display: flex; align-items: center; gap: 10px; }
.role-card-head b { font-size: 15.5px; color: var(--text); }
.role-mark {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 32px; height: 32px; border-radius: 9px;
  background: var(--accent-soft); color: var(--accent-text);
}

/* What the choice actually grants, said before it is made. */
.role-grants {
  display: flex; align-items: flex-start; gap: 7px; margin-top: 8px;
  padding-top: 10px; border-top: 1px solid var(--border);
  font-size: 12.5px; color: var(--accent-text);
}
.role-grants .ico { margin-top: 2px; }

#not-listed {
  border-left: 3px solid var(--border-strong);
  padding-left: 12px; max-width: 60ch;
}

/* ---------- the confirmation ---------- */

.confirm {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: var(--text);
  padding: 22px; max-width: 420px; width: calc(100% - 32px);
  box-shadow: 0 10px 40px rgba(0, 0, 0, .18);
}
.confirm::backdrop { background: rgba(20, 22, 26, .45); }
.confirm h2 { margin: 0 0 6px; }
.confirm .sub { margin-bottom: 14px; }

.confirm-warn {
  display: flex; gap: 9px; align-items: flex-start;
  background: var(--warn-soft); color: var(--warn);
  border-radius: 8px; padding: 11px 12px; font-size: 13px;
}
.confirm-warn-ico { flex: 0 0 auto; display: grid; place-items: center; }

.confirm-actions {
  display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px;
}
@media (max-width: 460px) {
  .confirm-actions { flex-direction: column-reverse; }
  .confirm-actions .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  .role-card { transition: none; }
  .role-card:hover, .role-card:active { transform: none; }
}

/* ---------- admin: changing someone's team ---------- */

.role-cell { display: flex; flex-direction: column; gap: 2px; min-width: 160px; }
.role-select {
  font: inherit; font-size: 13px; padding: 5px 8px;
  border: 1px solid var(--border); border-radius: 7px;
  background: var(--surface); color: var(--text); cursor: pointer;
  min-height: 30px;
}
.role-select:hover { border-color: var(--border-strong); }

/* ==================================================================
   Download activity
   ================================================================== */

.activity { color: var(--accent-text); }
.activity h3 { color: var(--text-faint); }
.chart-summary {
  color: var(--text); font-size: 14px; margin: 0 0 14px; max-width: 60ch;
}

/* The box is stretched to the card's width, so the path is drawn to fill
   it and the stroke width is held steady by vector-effect. */
.activity-plot { height: 140px; }
.activity-chart { display: block; width: 100%; height: 100%; }

.activity-axis {
  display: flex; justify-content: space-between;
  font-size: 11.5px; color: var(--text-dim); margin-top: 6px;
  padding-top: 8px; border-top: 1px solid var(--border);
}

/* The same numbers, readable by everything. */
.chart-data { margin-top: 14px; }
.chart-data summary {
  display: inline-flex; align-items: center; gap: 6px;
  cursor: pointer; font-size: 12.5px; color: var(--text-dim);
  list-style: none; min-height: 28px;
}
.chart-data summary::-webkit-details-marker { display: none; }
.chart-data summary:hover { color: var(--accent-text); }
.chart-data .table-wrap { margin: 8px 0 0; max-height: 240px; overflow: auto; }
.chart-data table { font-size: 13px; }
#dl-activity { margin-bottom: 16px; }
#dl-activity:empty { display: none; }

/* A section heading inside a card: the weight of an h3, at the level the
   document actually needs. */
.section-head {
  font-size: 13px; text-transform: uppercase; letter-spacing: .05em;
  color: var(--text-faint); font-weight: 600; margin: 0 0 10px;
}

/* ==================================================================
   The mark landing on the wordmark
   ------------------------------------------------------------------
   The logo is assembled rather than shipped flat: "remidio" as one
   image, the butterfly as another, positioned so its ink falls exactly
   in the gap the wordmark leaves (measured off the original: the mark
   occupies 79.4%-99.6% across, 0%-76% down).
   It is complete from the first frame. The flight is something that
   happens to a finished logo, never a condition of it being finished -
   so no script, no motion and no failed image leaves a hole in it.
   ================================================================== */

.brandmark {
  position: relative; display: block;
  height: 26px; aspect-ratio: 267 / 96;
}
.brandmark .wordmark {
  display: block; width: 100%; height: 100%; object-fit: contain;
}
.brandmark .wordmark.dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brandmark .wordmark.light { display: none; }
  :root:not([data-theme="light"]) .brandmark .wordmark.dark { display: block; }
}

/* The numbers below are measured, not guessed - they put the butterfly's
   ink at exactly x 212-266, y 0-73 of the original 267x96 logo. */
.brandmark-fly {
  position: absolute; left: 75.87%; top: 0;
  width: 27.39%; height: 76.04%;
  display: block;
}
.brandmark-fly img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* ---------- the landing ---------- */

/* Two nested movements make an arc out of two straight ones: the wrapper
   carries it across at an even rate while the inner element falls away
   upward on an ease.

   One duration drives all three animations. The wing beat is derived from
   it rather than set beside it, so the wings can never keep going after
   the landing or stop before it - which is what happens when two numbers
   that have to agree are written down twice. */
.brandmark-fly.landing {
  --bf-flight: 2800ms;
  animation: bf-cross var(--bf-flight) cubic-bezier(.35, 0, .25, 1) both;
}

/* In from the right, travelling left to the perch. The logo sits at the
   far left of the bar, so this crosses the whole header rather than
   appearing beside its destination - a longer approach, and the whole
   width of the page to read it in. */
@keyframes bf-cross {
  from { transform: translateX(92vw); }
  to   { transform: translateX(0); }
}
@keyframes bf-settle {
  /* Banked the other way now: the tilt follows the direction of travel,
     and it would read as flying backwards if it did not. */
  0%   { transform: translateY(44px) rotate(16deg) scale(2.6); opacity: 0; }
  12%  { opacity: 1; }
  /* Overshoots the perch and drops onto it, the way a landing actually
     goes - the only place in this app that overshoots at all. */
  76%  { transform: translateY(-9px) rotate(-5deg) scale(1.22); }
  100% { transform: translateY(0) rotate(0) scale(1); opacity: 1; }
}

/* Wings only while it is airborne: a finite count, so they stop on the
   perch rather than fluttering on forever. 14 beats at 200ms fills the
   2800ms flight exactly. A slower crossing gets a slower beat too - a
   butterfly that drifts is not also flapping frantically. */
.brandmark-fly.landing img {
  animation:
    bf-settle var(--bf-flight) cubic-bezier(.3, 0, .2, 1) both,
    bf-flap 200ms ease-in-out 14;
}
@keyframes bf-flap {
  0%, 100% { scale: 1 1; }
  50%      { scale: .62 1; }
}

@media (prefers-reduced-motion: reduce) {
  .brandmark-fly.landing,
  .brandmark-fly.landing img { animation: none; }
}

/* ---------- nothing published yet ---------- */

.empty-state { gap: 8px; padding: 34px 16px 30px; }

/* The mark, resting and faded well back - present enough to make the
   space feel intended, quiet enough not to read as a status. */
.empty-art {
  display: block; width: 46px; height: 46px; margin-bottom: 4px;
  opacity: .34;
}
.empty-art img { width: 100%; height: 100%; object-fit: contain; display: block; }
.empty-state .empty-title { font-size: 15px; }
.empty-hint {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 8px; font-size: 12px; color: var(--text-faint);
}

/* ---------- the refusal ---------- */

/* A closed eye, drawn from the same family as the sign-in one. */
.refusal-art {
  display: block; width: 96px; height: auto; margin: 0 auto 14px;
  color: var(--text-faint);
}
.refusal-art .lid, .refusal-art .lash {
  fill: none; stroke: currentColor; stroke-width: 3;
  stroke-linecap: round; stroke-linejoin: round;
}
.refusal-art .lash { stroke-width: 2.4; opacity: .6; }
