/* Flat hand-written CSS. No nesting, no remote imports. Colours come from tokens.css only. */

@layer reset, base, layout, components, utilities;

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  html {
    -webkit-text-size-adjust: 100%;
    text-size-adjust: 100%;
  }

  body,
  h1,
  h2,
  p,
  form {
    margin: 0;
  }

  button,
  input,
  textarea,
  select {
    font: inherit;
    color: inherit;
  }

  [hidden] {
    display: none !important;
  }
}

@layer base {
  :root {
    color-scheme: light dark;
  }

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

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

  body {
    background: var(--bg);
    color: var(--text);
    font: var(--fs-base) / 1.55 var(--font);
    min-height: 100vh;
    min-height: 100dvh;
  }

  h1,
  h2 {
    line-height: 1.2;
    text-wrap: balance;
  }

  h1 {
    font-size: var(--step-2);
    font-weight: 700;
  }

  h2 {
    font-size: var(--step-1);
    font-weight: 700;
  }

  a {
    color: var(--link);
  }

  input,
  textarea,
  select {
    font-size: max(1rem, 16px);
  }

  :focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
  }
}

@layer layout {
  body {
    display: flex;
    flex-direction: column;
    padding-left: env(safe-area-inset-left);
    padding-right: env(safe-area-inset-right);
  }

  /* Everything under the shell (or under the plain header): the page column. */
  .page {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
  }

  .site-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    border-bottom: 1px solid var(--border);
    background: var(--surface);
  }

  .header-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: var(--s2);
  }

  main {
    flex: 1;
    width: 100%;
    max-width: calc(var(--col) + 2 * var(--s4));
    margin: 0 auto;
    padding: var(--s5) var(--s4);
  }

  .stack {
    display: grid;
    gap: var(--s4);
  }

  .page-wide main {
    max-width: 80rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s4);
    align-content: start;
  }

  .site-footer {
    padding: var(--s2) var(--s4);
    padding-bottom: calc(var(--s2) + env(safe-area-inset-bottom));
    text-align: center;
    font-size: var(--step--1);
  }

  .site-footer a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
  }

  .card ul {
    margin: 0;
    padding-left: var(--s5);
    display: grid;
    gap: var(--s2);
  }
}

@layer components {
  .brand {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    min-width: 0;
    min-height: var(--tap);
    font-weight: 700;
    font-size: var(--step-1);
    color: var(--text);
    text-decoration: none;
  }

  .brand-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .mark {
    flex: none;
  }

  .mark-bg {
    fill: var(--accent);
  }

  .mark-glyph {
    fill: var(--on-accent);
  }

  .skip-link {
    position: absolute;
    left: var(--s4);
    top: -100px;
    padding: var(--s2) var(--s3);
    background: var(--surface);
    border-radius: var(--radius);
    z-index: 30;
  }

  .skip-link:focus {
    top: var(--s2);
  }

  .card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-1);
    padding: var(--s5);
    display: grid;
    gap: var(--s4);
  }

  .card-narrow {
    max-width: 26rem;
    margin: var(--s6) auto;
  }

  .login-providers {
    display: grid;
    gap: var(--s3);
  }

  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    min-height: var(--tap);
    padding: var(--s1) var(--s4);
    border: 1px solid transparent;
    border-radius: var(--radius-pill);
    text-align: center;
    background: transparent;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    transition:
      background-color var(--dur-1) var(--ease),
      box-shadow var(--dur-1) var(--ease),
      translate var(--dur-1) var(--ease),
      scale var(--dur-1) var(--ease);
  }

  .btn-primary {
    background: var(--accent);
    color: var(--on-accent);
  }

  .btn-quiet {
    border-color: var(--border-strong);
    color: var(--text);
  }

  @media (hover: hover) {
    .btn-quiet:hover {
      background: var(--accent-soft);
    }
  }

  .btn-danger {
    border-color: var(--danger);
    color: var(--danger);
  }

  .header-actions .btn {
    min-width: var(--tap);
    padding: 0 var(--s3);
  }

  .when-wide {
    display: none;
  }

  .field {
    display: grid;
    gap: var(--s1);
  }

  .field label {
    font-weight: 600;
  }

  .field input,
  .field select {
    min-height: var(--tap);
    padding: 0 var(--s3);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
  }

  .field input[aria-invalid="true"],
  .field select[aria-invalid="true"] {
    border-color: var(--danger);
  }

  .field-error,
  .form-error {
    color: var(--danger);
    font-weight: 600;
  }

  .muted {
    color: var(--text-muted);
  }

  .account-details {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: var(--s2) var(--s4);
    margin: 0;
  }

  .account-details dt {
    color: var(--text-muted);
  }

  .account-details dd {
    margin: 0;
    overflow-wrap: anywhere;
  }

  /* The segmented controls: the Account page's choices and the chat's range switch share this look.
     The current segment sits on the surface with a shadow, a 2px accent border and bold text, so
     colour is never the only cue. */
  .choice-group {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin: 0 0 var(--s3);
    padding: 0;
    border: 0;
  }

  .choice-group legend {
    padding: 0;
    margin-bottom: var(--s2);
    font-weight: 600;
  }

  .btn-choice {
    min-height: var(--tap);
    border-color: transparent;
    background: var(--surface-2);
    color: var(--text);
  }

  .choice-group [aria-pressed="true"] {
    border: 2px solid var(--accent);
    background: var(--surface);
    color: var(--text);
    font-weight: 700;
    box-shadow: var(--shadow-1);
  }

  .range-switch {
    display: inline-flex;
    flex-wrap: wrap;
    max-width: 100%;
    gap: 2px;
    padding: var(--s1);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    font-size: 1rem;
  }

  .range-switch a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--s3);
    border: 2px solid transparent;
    border-radius: var(--radius-pill);
    color: var(--text);
    text-decoration: none;
    white-space: nowrap;
  }

  .range-switch a[aria-current] {
    border-color: var(--accent);
    background: var(--surface);
    font-weight: 700;
    box-shadow: var(--shadow-1);
  }

  /* With range-switch.js the nav gets .has-indicator and a pill that slides under the links. The
     links paint above the pill (z-index -1), so while it is there they stay transparent, hovered
     or not, and the current one drops its own pill (its bold weight stays). The hover cue is an
     underline. Without the script the plain links keep the flat look above. */
  .range-switch.has-indicator {
    position: relative;
    isolation: isolate;
  }

  .range-switch.has-indicator a {
    background: transparent;
    transition: scale var(--dur-1) var(--ease);
  }

  .range-switch.has-indicator a[aria-current] {
    border-color: transparent;
    box-shadow: none;
  }

  @media (hover: hover) {
    .range-switch.has-indicator a:not([aria-current]):hover {
      text-decoration-line: underline;
      text-underline-offset: 0.25em;
    }
  }

  .range-indicator {
    position: absolute;
    top: 0;
    left: 0;
    z-index: -1;
    width: var(--ri-w, 0);
    height: var(--ri-h, 0);
    transform: translate(var(--ri-x, 0), var(--ri-y, 0));
    border: 2px solid var(--accent);
    border-radius: var(--radius-pill);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    pointer-events: none;
    transition:
      transform var(--dur-slide) var(--ease-spring),
      width var(--dur-slide) var(--ease),
      height var(--dur-slide) var(--ease);
  }

  .range-indicator.is-placing {
    transition: none;
  }

  .banner {
    padding: var(--s3) var(--s4);
    border-radius: var(--radius-lg);
    background: var(--banner-bg);
    color: var(--banner-text);
  }

  .credentials {
    display: grid;
    gap: var(--s1);
    margin: 0;
  }

  .credentials dt {
    font-weight: 600;
  }

  .credentials dd {
    margin: 0 0 var(--s3);
    overflow-wrap: anywhere;
  }

  .admin-nav {
    display: flex;
    gap: var(--s1);
    padding-bottom: var(--s1);
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
  }

  .admin-nav a {
    display: inline-flex;
    flex: none;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--s4);
    border-bottom: 3px solid transparent;
    color: var(--text-muted);
    text-decoration: none;
    white-space: nowrap;
  }

  .admin-nav a[aria-current] {
    border-bottom-color: var(--accent);
    color: var(--text);
    font-weight: 700;
  }

  .table-wrap {
    container-type: inline-size;
    overflow-x: auto;
  }

  .table-wrap:focus-visible {
    outline: 3px solid var(--focus);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
  }

  .data-table {
    width: 100%;
    border-collapse: collapse;
  }

  .data-table th,
  .data-table td {
    padding: var(--s2);
    border-bottom: 1px solid var(--border);
    text-align: left;
    vertical-align: top;
  }

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

  .data-table tfoot th,
  .data-table tfoot td {
    border-top: 2px solid var(--border-strong);
    font-weight: 600;
  }

  /* Numbers, dates, model ids and badges never wrap, in both modes (a badge stays a pill; the
     status cell stacks the two badges). Names, headers and the actions wrap at spaces (the month
     cell's figures wrap between them, each one whole). French digit groups use U+202F, which
     never breaks. */
  .data-table td.num,
  .data-table .cell-date,
  .data-table .cell-code,
  .badge,
  .who .id {
    white-space: nowrap;
  }

  /* A part of an email is an atomic box, so the line breaks between the parts (the `<wbr>` after
     each) and never at a hyphen inside one. Only a part wider than its cell breaks inside itself,
     and so does a name with no space: `overflow-wrap: anywhere` is the last resort. */
  .who {
    display: grid;
    min-width: 0;
  }

  .who a,
  .email > span {
    overflow-wrap: anywhere;
  }

  .email > span {
    display: inline-block;
    max-width: 100%;
  }

  /* The accounts list's status cell (role and status badges) and its month cell (three labelled
     figures that wrap onto two lines, the tokens pair last). */
  .badges,
  .month {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s2);
    margin: 0;
  }

  .month {
    column-gap: var(--s3);
  }

  .month > div {
    display: flex;
    gap: var(--s1);
    white-space: nowrap;
  }

  .month dt {
    color: var(--text-muted);
  }

  .month dd {
    margin: 0;
    font-variant-numeric: tabular-nums;
  }

  /* Wide enough for Approve and Reject on one line (the card blocks reset it). The accounts list's
     table mode wraps its controls onto two short lines instead: see the
     `@container (width > 60rem)` block below. */
  .data-table .cell-actions {
    min-width: 14rem;
  }

  /* Six cards: one compact row each on a phone, two columns, then three (3 + 3, never an orphan). */
  .kpis {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s3);
    margin: 0;
  }

  .kpi {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s3);
    min-width: 0;
    padding: var(--s3) var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-1);
  }

  .kpi dt {
    color: var(--text-muted);
    font-size: var(--step--1);
  }

  .kpi dd {
    margin: 0;
    font-size: var(--step-1);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
  }

  /* A chart is drawn from the table beside it (admin-charts.js): the figure adds no padding or
     margin, so the table's wrapper is as wide as a bare one. */
  figure.chart {
    margin: 0;
    min-width: 0;
    container-type: inline-size;
  }

  /* The figure is the container of the table inside it, so the card switch and the scroller of a
     drawn daily table (below the layer) are asked about the same width. */
  figure.chart .table-wrap {
    container-type: normal;
  }

  .chart-box {
    position: relative;
    min-width: 0;
    height: 16rem;
    margin-bottom: var(--s3);
    padding: var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
  }

  figure.chart[data-chart="doughnut"] .chart-box {
    height: 15rem;
  }

  figure.chart[data-chart="bar"] .chart-box {
    height: 12rem;
  }

  .window-nav,
  .section-nav,
  .pager {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2) var(--s4);
    margin: var(--s4) 0;
  }

  .window-nav a[aria-current] {
    font-weight: 700;
    color: var(--text);
  }

  /* The dashboard's links to its own sections: a row that wraps, a tap-sized link each. The
     anchors land under the sticky bar through the root's scroll-padding-top (the repo has no
     scroll-margin-top, a lint test says so). */
  .section-nav {
    margin-top: 0;
  }

  .section-nav a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
  }

  /* A table that a chart already draws sits in a disclosure, closed by admin-charts.js once the
     chart is there; open, it is the table as ever (without the script, it never closes). */
  .table-details > summary {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-height: var(--tap);
    padding: 0 var(--s2);
    border-radius: var(--radius);
    color: var(--text-muted);
    list-style: none;
    cursor: pointer;
  }

  .table-details > summary::-webkit-details-marker {
    display: none;
  }

  .table-details > summary::before {
    content: "";
    flex: none;
    width: 0.5em;
    height: 0.5em;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    rotate: -45deg;
  }

  .table-details[open] > summary::before {
    rotate: 45deg;
  }

  /* Flat bars. accent-color does not reach <meter>; WebKit/Blink and Gecko each style their own
     pseudo-elements, in separate rules because one unknown selector drops a whole rule. */
  meter {
    appearance: none;
    width: 5rem;
    height: 0.75rem;
    margin-inline-start: var(--s2);
    vertical-align: middle;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface-2);
    overflow: hidden;
  }

  meter::-webkit-meter-bar {
    background: none;
    border: 0;
  }

  meter::-webkit-meter-optimum-value {
    background: var(--accent);
  }

  meter::-moz-meter-bar {
    background: var(--accent);
  }

  .badge {
    display: inline-block;
    padding: 0 var(--s2);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    font-size: 0.9em;
  }

  .badge-off {
    border-color: var(--danger);
    color: var(--danger);
  }

  .actions,
  .inline-form,
  .dialog-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
  }

  .actions .btn {
    padding: 0 var(--s3);
  }

  .actions select,
  .inline-form select {
    min-height: var(--tap);
    padding: 0 var(--s2);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface);
  }

  .flash {
    margin: 0 0 var(--s4);
    padding: var(--s3) var(--s4);
    border-radius: var(--radius-lg);
    background: var(--banner-bg);
    color: var(--banner-text);
  }

  /* The chat page has no padding on main, so its flash gets its own margin. */
  .page-chat .flash {
    margin: var(--s3) var(--s4) 0;
  }
}

/* Tables turn into cards when their wrapper is narrower than a threshold: 40rem for the narrow
   tables and 60rem for the accounts list (`.is-list`), and always for the `.is-wide` ones (the
   models table and the ranges table: they are cards at every width and every root font size, so
   no number here depends on the cap of `main`). The list is a compact table of five columns
   (account, status, last activity, this month, actions) whose minimum width is 795px (50rem) at
   most, in French or English with 55 accounts and a 55-character email, at the 18px base over
   seven installed fonts (DejaVu Sans 771px, Montserrat 764px, Liberation Sans 712px, Open Sans
   726px, Droid Sans 712px, Noto Sans 729px, monospace 795px): a table above 60rem (the wrapper is
   61rem, 976px, at 1280 px, so at least 18% of slack in the widest font; 78rem at 1600 px), cards
   from 60rem down (41rem at 960 px). The three blocks hold the same declarations (tests/test_frontend_lint.py
   checks it). They are unlayered so they beat
   the table rules above, and each card sits in a grid, two or three per row on a laptop. The
   queries of a table inside a `figure.chart` resolve against the figure (see below), which is as
   wide as the wrapper. */
@container (max-width: 40rem) {
  .data-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  .data-table,
  .data-table tbody,
  .data-table tfoot,
  .data-table tr,
  .data-table td,
  .data-table th[scope="row"] {
    display: block;
  }

  .data-table tbody {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
    gap: var(--s3);
  }

  .data-table tfoot tr {
    margin-top: var(--s3);
  }

  .data-table tr {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
  }

  .data-table td {
    display: flex;
    justify-content: flex-end;
    gap: var(--s3);
    border-bottom: 1px solid var(--border);
  }

  .data-table td > * {
    min-width: 0;
  }

  .data-table td::before {
    content: attr(data-label);
    margin-inline-end: auto;
    font-weight: 600;
  }

  .data-table .who {
    text-align: right;
  }

  .data-table .badges,
  .data-table .month {
    justify-content: flex-end;
  }

  .data-table .cell-month::before {
    flex: none;
  }

  .data-table .cell-actions {
    flex-wrap: wrap;
    min-width: 0;
  }
}

/* The `.is-wide` tables: the same card declarations, with no query. */
.data-table.is-wide thead {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.data-table.is-wide,
.data-table.is-wide tbody,
.data-table.is-wide tfoot,
.data-table.is-wide tr,
.data-table.is-wide td,
.data-table.is-wide th[scope="row"] {
  display: block;
}

.data-table.is-wide tbody {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  gap: var(--s3);
}

.data-table.is-wide tfoot tr {
  margin-top: var(--s3);
}

.data-table.is-wide tr {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.data-table.is-wide td {
  display: flex;
  justify-content: flex-end;
  gap: var(--s3);
  border-bottom: 1px solid var(--border);
}

.data-table.is-wide td > * {
  min-width: 0;
}

.data-table.is-wide td::before {
  content: attr(data-label);
  margin-inline-end: auto;
  font-weight: 600;
}

.data-table.is-wide .who {
  text-align: right;
}

.data-table.is-wide .badges,
.data-table.is-wide .month {
  justify-content: flex-end;
}

.data-table.is-wide .cell-month::before {
  flex: none;
}

.data-table.is-wide .cell-actions {
  flex-wrap: wrap;
  min-width: 0;
}

@container (max-width: 60rem) {
  .data-table.is-list thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }

  .data-table.is-list,
  .data-table.is-list tbody,
  .data-table.is-list tfoot,
  .data-table.is-list tr,
  .data-table.is-list td,
  .data-table.is-list th[scope="row"] {
    display: block;
  }

  .data-table.is-list tbody {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
    gap: var(--s3);
  }

  .data-table.is-list tfoot tr {
    margin-top: var(--s3);
  }

  .data-table.is-list tr {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
  }

  .data-table.is-list td {
    display: flex;
    justify-content: flex-end;
    gap: var(--s3);
    border-bottom: 1px solid var(--border);
  }

  .data-table.is-list td > * {
    min-width: 0;
  }

  .data-table.is-list td::before {
    content: attr(data-label);
    margin-inline-end: auto;
    font-weight: 600;
  }

  .data-table.is-list .who {
    text-align: right;
  }

  .data-table.is-list .badges,
  .data-table.is-list .month {
    justify-content: flex-end;
  }

  .data-table.is-list .cell-month::before {
    flex: none;
  }

  .data-table.is-list .cell-actions {
    flex-wrap: wrap;
    min-width: 0;
  }
}

/* The accounts list in table mode: the plan select, Save and Delete (or Approve and Reject) wrap
   onto a second short line when the column is narrow instead of widening it, the badges stack,
   the text is a little smaller and no cell has a minimum width of its own, so the table's width
   is set by its widest unbreakable piece (a button, a badge, a date, a figure of the month). */
@container (width > 60rem) {
  .data-table.is-list .actions .btn {
    padding: 0 var(--s2);
    white-space: nowrap;
  }

  .data-table.is-list .cell-actions {
    min-width: 13.5rem;
  }

  /* A pending account's plan line takes the first line, Approve and Reject the second. */
  .data-table.is-list .actions > span {
    flex: 1 0 100%;
  }

  .data-table.is-list .badges {
    flex-direction: column;
    align-items: flex-start;
  }

  .data-table.is-list th,
  .data-table.is-list td {
    font-size: 0.9em;
  }
}

/* A drawn daily chart's table is long (30 rows): from the width where it is a table it scrolls
   inside its wrapper, under a sticky header. The scroller's query and the narrow tables' card
   switch (`@container (max-width: 40rem)`, above) resolve against the same container, the figure
   (the wrapper inside a figure is not a container), and this is the exact negation of that
   switch: no width has both cards and a scroller, whatever width the scroller's own scrollbar
   takes off the wrapper. */
@container not (max-width: 40rem) {
  figure.chart[data-chart="daily"].is-drawn .table-wrap {
    max-height: 18rem;
    overflow-y: auto;
  }

  figure.chart[data-chart="daily"].is-drawn .data-table thead th {
    position: sticky;
    top: 0;
    background: var(--surface);
  }
}

@media (min-width: 26rem) {
  .kpis {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .kpi {
    flex-direction: column;
    align-items: stretch;
    gap: var(--s1);
    padding: var(--s4);
  }

  .kpi dd {
    font-size: var(--step-2);
  }
}

@media (min-width: 40rem) {
  .kpis {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (min-width: 40rem) {
  .card {
    padding: var(--s6);
  }

  .empty-title {
    font-size: var(--step-3);
  }

  .when-compact {
    display: none;
  }

  .when-wide {
    display: inline;
  }

  .header-actions .btn {
    padding: 0 var(--s4);
  }
}

/* The sidebar's tools row is compact at every width. Unlayered on purpose: the .when-* rules above
   are unlayered too, and a rule in a layer would lose to them. */
.sidebar-tools .when-wide {
  display: none;
}

.sidebar-tools .when-compact {
  display: inline;
}

@layer utilities {
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
}

/* The selectors repeat tokens.css's three theme blocks: the dark ones are more specific than a
   plain :root, and this file loads after it. */
@media (prefers-contrast: more) {
  :root,
  :root:not([data-theme="light"]),
  :root[data-theme="dark"] {
    --border: var(--border-strong);
  }
}

@media (forced-colors: active) {
  .btn,
  .field input,
  .field select {
    border: 2px solid ButtonText;
  }

  .app-nav a[aria-current],
  .admin-nav a[aria-current] {
    text-decoration: underline;
    border-bottom-color: Highlight;
  }

  .choice-group [aria-pressed="true"] {
    border: 4px solid Highlight;
  }

  #range-switch a[aria-current] {
    border: 4px solid Highlight;
  }

  .demo-chip[aria-pressed="true"] {
    border: 4px solid Highlight;
  }

  .demo-chip,
  .demo,
  .landing-card {
    border: 1px solid CanvasText;
  }

  .safety-steps li::before,
  .landing-card .icon-fill {
    forced-color-adjust: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }

  /* The whole switch (labels and pill) is one captured element: range-switch.js has already slid
     the pill, so the old and new images match and the switch stays solid while the page
     cross-fades. Naming the empty pill alone would paint it over the current label. Only a range
     click morphs the switch: the old page names it with .is-leaving (set by the click), and the
     script marks it .is-still on any other navigation, so New chat or a link just shows the next
     page. The new page names it while its transition runs. Both rules need what only the script
     sets (.has-indicator on the new page, .is-leaving or .is-still on the old one), so without
     the script nothing is named. Two rules, because a browser without :active-view-transition
     would drop a selector list. */
  #range-switch.is-leaving {
    view-transition-name: range-switch;
  }

  :root:active-view-transition #range-switch.has-indicator:not(.is-still) {
    view-transition-name: range-switch;
  }
}

/* The app shell: the rail holds the bar (brand, New chat, menu button) and the sidebar (saved
   chats, navigation, language, logout). Below 56rem the bar is a top bar and the sidebar a drawer
   (a popover); from 56rem it is a left column. The document stays the scroller. */

@layer components {
  .rail {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
  }

  .rail-bar {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s2) var(--s3);
  }

  .rail-bar .brand {
    flex: 1 1 auto;
  }

  .btn-new {
    flex: none;
    min-width: var(--tap);
    padding: 0 var(--s3);
    background: var(--accent);
    color: var(--on-accent);
  }

  .menu-button {
    display: none;
    position: relative;
    flex: none;
    min-width: var(--tap);
    padding: 0 var(--s3);
  }

  .sidebar {
    padding: var(--s3);
    background: var(--sidebar);
    color: var(--text);
    flex-direction: column;
    gap: var(--s4);
  }

  /* The first row of the sidebar: the language toggle and logout as compact icon buttons at the
     end. The compact look in every layout is the unlayered override after the 40rem query. */
  .sidebar-tools {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    gap: var(--s2);
  }

  .sidebar-tools .btn {
    min-width: var(--tap);
    padding: 0 var(--s3);
  }

  .app-nav {
    display: grid;
    gap: var(--s1);
  }

  .app-nav a {
    display: flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--s3);
    border-radius: var(--radius-pill);
    color: var(--text);
    font-size: 1rem;
    text-decoration: none;
  }

  .nav-count,
  .menu-dot {
    border: 1px solid var(--accent);
    border-radius: var(--radius-pill);
    background: var(--accent);
    color: var(--on-accent);
  }

  .nav-count {
    display: inline-block;
    min-width: 1.5em;
    margin-inline-start: var(--s2);
    padding: 0 var(--s2);
    font-size: var(--step--1);
    font-weight: 700;
    line-height: 1.5;
    text-align: center;
  }

  .menu-dot {
    position: absolute;
    top: var(--s2);
    right: var(--s2);
    width: 0.75rem;
    height: 0.75rem;
  }

  .app-nav a[aria-current] {
    background: var(--accent-soft);
    font-weight: 700;
  }
}

/* A browser with popover: the drawer is closed (hidden by the browser) until the menu button
   opens it. This is the only place `.sidebar` gets a display below 56rem. */
@supports selector(:popover-open) {
  @media not all and (min-width: 56rem) {
    html {
      scroll-padding-top: 5rem;
    }

    .rail {
      position: sticky;
      top: 0;
      z-index: 20;
    }

    .menu-button {
      display: inline-flex;
    }

    .sidebar:popover-open {
      display: flex;
      position: fixed;
      inset: 0 auto 0 0;
      width: min(20rem, 88vw);
      height: 100dvh;
      max-height: none;
      margin: 0;
      padding: var(--s4);
      padding-top: max(var(--s4), env(safe-area-inset-top));
      border: 0;
      border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
      overflow-y: auto;
      box-shadow: var(--shadow-2);
      transform: none;
      transition: transform var(--dur-2) var(--ease);
    }

    /* The drawer scrolls as a whole only on a window too short for the minimum list; otherwise
       the saved chats list takes the room left under the controls and is the scroller. */
    .sidebar:popover-open .history {
      display: flex;
      flex-direction: column;
      flex: 1 1 auto;
      min-height: 0;
    }

    .sidebar::backdrop {
      background: var(--backdrop);
    }

    @starting-style {
      .sidebar:popover-open {
        transform: translateX(-100%);
      }
    }
  }
}

/* iPadOS 16.4 and older: no popover, so the sidebar stacks under the bar. The menu button never
   shows there, so it cannot be a dead button. */
@supports not selector(:popover-open) {
  .sidebar {
    display: flex;
    border-bottom: 1px solid var(--border);
  }

  .sidebar .history-list {
    max-height: 14rem;
  }
}

@media (min-width: 56rem) {
  /* The column's list fills what the controls leave, also where the no-popover cap above applies. */
  .sidebar .history-list {
    max-height: none;
  }

  body.has-shell {
    display: grid;
    grid-template-columns: var(--rail) minmax(0, 1fr);
    --shell-inset: var(--rail);
  }

  .rail {
    position: sticky;
    top: 0;
    align-self: start;
    height: 100vh;
    height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    border-right: 1px solid var(--border);
    border-bottom: 0;
    background: var(--sidebar);
  }

  .rail-bar {
    display: grid;
    gap: var(--s3);
    padding: var(--s4) var(--s3) 0;
  }

  .rail-bar .brand {
    padding: 0 var(--s2);
  }

  .btn-new {
    justify-content: center;
  }

  .sidebar {
    display: flex;
    flex: 1 1 auto;
    position: static;
    inset: auto;
    width: auto;
    height: auto;
    max-height: none;
    margin: 0;
    padding: var(--s4) var(--s3);
    border: 0;
    border-radius: 0;
    overflow: visible;
    min-height: 0;
    background: transparent;
    color: var(--text);
    box-shadow: none;
    transform: none;
  }

  .history {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
  }
}

/* Chat page */

@layer layout {
  .page-chat main {
    max-width: none;
    padding: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    align-content: stretch;
  }

  /* A flash takes the first (auto) row and the chat always the second, whether or not there is
     a flash, so the composer stays pinned to the bottom. */
  .page-chat main > .flash {
    grid-row: 1;
  }

  .page-chat main > .chat {
    grid-row: 2;
  }

  .chat {
    width: 100%;
    max-width: calc(var(--col) + 2 * var(--s4));
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .chat-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2) var(--s3);
    padding: var(--s3) var(--s4) 0;
  }

  .thread {
    flex: 1;
    list-style: none;
    margin: 0;
    padding: var(--s4);
    display: grid;
    gap: var(--s5);
    align-content: start;
  }

  .composer {
    position: sticky;
    bottom: 0;
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--tap);
    align-items: end;
    row-gap: var(--s2);
    padding: var(--s5) var(--s4) var(--s2);
    padding-bottom: calc(max(var(--s2), env(safe-area-inset-bottom)) + var(--kb-inset, 0px));
    background: linear-gradient(to bottom, transparent, var(--bg) var(--s5));
  }

  .composer > .composer-error {
    grid-column: 1 / -1;
    grid-row: 1;
  }

  .composer > textarea {
    grid-column: 1 / -1;
    grid-row: 2;
  }

  .composer > .send {
    grid-column: 2;
    grid-row: 2;
    justify-self: end;
    align-self: end;
    margin: 0 var(--s2) var(--s2) 0;
  }

  .quota-card {
    margin: var(--s4);
  }
}

@layer components {
  .pill {
    display: inline-block;
    padding: var(--s1) var(--s3);
    border-radius: var(--radius-pill);
    font-size: var(--step--1);
    color: var(--text-muted);
    background: var(--surface-2);
  }

  .msg {
    display: block;
    max-width: 100%;
    transition:
      opacity var(--dur-2) var(--ease),
      transform var(--dur-2) var(--ease);
  }

  .msg-user {
    justify-self: end;
    max-width: min(85%, 36rem);
  }

  /* An assistant reply sits beside the mascot; the child layout below gives it its bubble. */
  .msg-assistant {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: var(--s3);
    justify-self: stretch;
  }

  .bubble {
    min-width: 0;
  }

  .msg-user .bubble {
    padding: var(--s3) var(--s4);
    border-radius: var(--radius-bubble);
    background: var(--user-bubble);
    color: var(--text);
  }

  .msg-text {
    margin: 0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }

  .notice {
    margin: var(--s2) 0 0;
    font-size: 1rem;
    color: var(--text-muted);
  }

  .chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-top: var(--s2);
  }

  .chip {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--s3);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-pill);
    background: var(--surface);
  }

  .help-card {
    display: grid;
    gap: var(--s3);
    padding: var(--s4);
    border: 2px solid var(--help-text);
    border-radius: var(--radius-lg);
    background: var(--help-bg);
    color: var(--help-text);
  }

  .help-title {
    margin: 0;
    font-size: var(--step-1);
  }

  .help-list {
    display: grid;
    gap: var(--s3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .help-entry {
    display: grid;
    gap: var(--s1);
    justify-items: start;
    padding-top: var(--s3);
    border-top: 1px solid var(--help-text);
  }

  .help-entry p {
    margin: 0;
  }

  .help-name {
    font-weight: 600;
  }

  .help-call {
    border: 2px solid var(--help-text);
    color: var(--help-text);
    font-weight: 700;
  }

  .help-site {
    color: var(--help-text);
  }

  .help-urgent .help-call {
    border-width: 4px;
    font-size: 1.15em;
  }

  .retry {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s2);
    margin-top: var(--s2);
    font-weight: 600;
  }

  .msg-user .retry .btn {
    background: var(--surface);
  }

  .empty-state {
    padding: var(--s7) var(--s4) var(--s4);
    display: grid;
    gap: var(--s3);
    justify-items: center;
    align-content: start;
    text-align: center;
  }

  .empty-title {
    font-size: var(--step-2);
  }

  .empty-state > p {
    color: var(--text-muted);
  }

  .ideas {
    width: 100%;
    margin-top: var(--s4);
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: var(--s3);
  }

  .idea {
    min-height: var(--tap);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    cursor: pointer;
    text-align: left;
    transition:
      background-color var(--dur-1) var(--ease),
      border-color var(--dur-1) var(--ease),
      box-shadow var(--dur-1) var(--ease),
      translate var(--dur-1) var(--ease),
      scale var(--dur-1) var(--ease);
  }

  @media (hover: hover) {
    .idea:hover {
      border-color: var(--accent);
    }
  }

  .typing {
    display: flex;
    align-items: center;
    gap: var(--s1);
    padding: var(--s2) var(--s4);
  }

  .typing span {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--text-muted);
    animation: typing-dot 1.2s infinite ease-in-out;
  }

  .typing span:nth-of-type(2) {
    animation-delay: 0.2s;
  }

  .typing span:nth-of-type(3) {
    animation-delay: 0.4s;
  }

  .composer textarea {
    min-height: calc(var(--tap) + var(--s3));
    max-height: 40vh;
    padding: var(--s3) calc(var(--tap) + var(--s4)) var(--s3) var(--s4);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-2);
    line-height: 1.4;
    resize: none;
  }

  .send {
    width: var(--tap);
    min-width: var(--tap);
    padding: 0;
  }

  .new-pill {
    position: fixed;
    left: var(--shell-inset, 0px);
    right: 0;
    bottom: calc(7rem + env(safe-area-inset-bottom));
    width: fit-content;
    margin-inline: auto;
    box-shadow: var(--shadow-2);
  }
}

/* The saved chats list, in the sidebar. The head and the privacy disclosure stay put; the list is
   the only part that scrolls, in the column, the drawer and the fallback alike (the flex layout is
   in the unlayered shell blocks above, which a layered rule could not override). */

@layer components {
  .history {
    display: grid;
    gap: var(--s2);
  }

  .history-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
  }

  .history h2 {
    font-size: var(--step--1);
    color: var(--text-muted);
    padding: 0 var(--s2);
  }

  .history h2:focus-visible {
    outline-offset: 0;
  }

  /* The list scrolls, so it clips the 5px focus ring (3px outline, 2px offset) of its rows: the
     padding makes room for it and the negative margin keeps the rows where they were. */
  .history-list {
    flex: 1 1 auto;
    min-height: calc(3 * var(--tap));
    margin: calc(-1 * var(--s2));
    padding: var(--s2);
    overflow-y: auto;
    overscroll-behavior: contain;
    list-style: none;
    display: grid;
    gap: var(--s1);
    align-content: start;
  }

  /* position: relative keeps the visually hidden label of a row inside the scroller: an absolute
     box would resolve against the rail or the drawer and add to their scroll height. */
  .history-row {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--tap);
    align-items: start;
    border-radius: var(--radius);
  }

  .history-link {
    min-width: 0;
    min-height: var(--tap);
    padding: var(--s2);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    column-gap: var(--s2);
    color: var(--link);
    text-decoration: none;
  }

  .history-link[aria-current] {
    font-weight: 700;
  }

  .history-range,
  .history-date {
    color: var(--text-muted);
    font-size: var(--step--1);
    white-space: nowrap;
  }

  .history-excerpt {
    flex-basis: 100%;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    overflow-wrap: anywhere;
  }

  .history-delete,
  .history-delete-all {
    flex: none;
    width: var(--tap);
    min-width: var(--tap);
    height: var(--tap);
    padding: 0;
    border-color: transparent;
    color: var(--text-muted);
    transition:
      background-color var(--dur-1) var(--ease),
      box-shadow var(--dur-1) var(--ease),
      opacity var(--dur-1) var(--ease),
      translate var(--dur-1) var(--ease),
      scale var(--dur-1) var(--ease);
  }

  .history-delete svg {
    width: 18px;
    height: 18px;
  }

  .history-delete-all svg {
    width: 20px;
    height: 20px;
  }

  .history-about summary {
    display: flex;
    align-items: center;
    gap: var(--s2);
    min-height: var(--tap);
    padding: 0 var(--s2);
    border-radius: var(--radius);
    color: var(--text-muted);
    font-size: var(--step--1);
    list-style: none;
    cursor: pointer;
  }

  .history-about summary::-webkit-details-marker {
    display: none;
  }

  /* display: flex removes the native triangle, so the summary draws a chevron that turns when it
     opens. Borders keep their colour in forced colours and the size follows the text. */
  .history-about summary::before {
    content: "";
    flex: none;
    width: 0.5em;
    height: 0.5em;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    rotate: -45deg;
  }

  .history-about[open] summary::before {
    rotate: 45deg;
  }

  .history-note {
    margin: 0;
    padding: var(--s1) var(--s2) var(--s2);
    font-size: var(--step--1);
  }

  /* The row's delete button is hidden only with a fine primary pointer and no coarse pointer at
     all, so a touch screen (a phone, or a hybrid laptop) never taps an invisible button. The
     hidden button stays focusable and clickable: the row's hover and focus reveal it. */
  @media (hover: hover) and (pointer: fine) {
    @media not all and (any-pointer: coarse) {
      .history-delete {
        opacity: 0;
      }

      .history-row:hover .history-delete,
      .history-row:focus-within .history-delete,
      .history-delete:focus-visible {
        opacity: 1;
      }
    }
  }
}

@keyframes typing-dot {
  0%,
  80%,
  100% {
    opacity: 0.3;
  }

  40% {
    opacity: 1;
  }
}

@supports (field-sizing: content) {
  .composer textarea {
    field-sizing: content;
    max-height: 40vh;
  }
}

@media (prefers-reduced-motion: no-preference) {
  @starting-style {
    .msg {
      opacity: 0;
      transform: translateY(8px);
    }
  }
}

@media (forced-colors: active) {
  .msg-user .bubble,
  .idea,
  .chip,
  .pill,
  .composer textarea {
    border: 1px solid CanvasText;
  }

  .badge,
  .mark-bg {
    forced-color-adjust: none;
  }

  .chart-box {
    display: none;
  }

  figure.chart[data-chart="daily"].is-drawn .table-wrap {
    max-height: none;
  }

  meter {
    forced-color-adjust: none;
    border-color: CanvasText;
    background: Canvas;
  }

  meter::-webkit-meter-optimum-value {
    background: Highlight;
  }

  meter::-moz-meter-bar {
    background: Highlight;
  }

  .history-link[aria-current] {
    text-decoration: underline;
  }

  .history-delete {
    opacity: 1;
  }

  .range-indicator {
    display: none;
  }
}

/* Child mode: rounder and warmer, with the mascot. Sizes are the same as the parent's. */

@layer components {
  .mascot {
    flex: none;
    color: var(--accent);
  }

  .mascot-eye {
    fill: var(--on-accent);
  }

  .mascot-line,
  .mascot-mouth {
    fill: none;
    stroke: currentColor;
    stroke-linecap: round;
    stroke-linejoin: round;
    stroke-width: 3;
  }

  .mascot-mouth {
    stroke: var(--on-accent);
  }

  .mascot-cheek {
    fill: var(--warm);
  }

  .chat > .banner {
    margin: var(--s3) var(--s4) 0;
  }

  .mode-child .msg-assistant,
  .demo-example .msg-assistant {
    display: flex;
    align-items: flex-start;
    gap: var(--s3);
  }

  .mode-child .msg-assistant .bubble,
  .demo-example .msg-assistant .bubble {
    padding: var(--s3) var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--radius-bubble);
    background: var(--surface);
  }

  .mode-child .typing .mascot,
  .demo .typing .mascot {
    margin-right: var(--s2);
  }

  .mode-child .quota-card {
    display: flex;
    align-items: center;
    gap: var(--s4);
    padding: var(--s4) var(--s5);
    border-radius: var(--radius-lg);
    background: var(--surface-2);
  }
}

@keyframes mascot-bob {
  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-4px);
  }
}

@media (prefers-reduced-motion: no-preference) {
  .mode-child .typing .mascot,
  .demo .typing .mascot {
    animation: mascot-bob 1.2s ease-in-out infinite;
  }
}

/* Landing page (/ and /about): its own sticky header, a hero with the pre-recorded demo, and the
   sections. Mode-free: the body is `mode-anon` for every viewer, and the demo's child rows borrow
   the chat's child layout through `.demo-example`, not a mode class. */

/* The landing page scrolls the document, and its sticky header covers the top --landing-header-h of
   it. The one offset is the scroll-padding-top of the root, so anchors, the skip link and focused
   elements all land below the header. It is unlayered and more specific on purpose: it replaces the
   shell's own root offset (the drawer's bar) instead of adding to it, and the landing has no shell.
   The header's height depends on the device's fonts, so landing.js measures it and writes the exact
   px value to --landing-header-h on the root; the value here is only the estimate without
   JavaScript. --landing-header-min is the header's own minimum height and is never written by the
   script, so the measured value cannot hold the header open. */
html:has(body.page-landing) {
  --landing-header-min: 7.5rem;
  --landing-header-h: var(--landing-header-min);
  scroll-padding-top: calc(var(--landing-header-h) + 1rem);
}

/* Below 49rem the nav can wrap (a larger root font, or wider device fonts), up to four rows of
   about 3rem each: without JavaScript the estimate covers that, at the cost of a roomier gap above
   an anchored heading when the header is only two rows. The viewport height caps it, so a short
   landscape phone keeps the anchored heading on screen, and the header's own minimum height is
   the floor. */
@media not all and (min-width: 49rem) {
  html:has(body.page-landing) {
    --landing-header-h: max(var(--landing-header-min), min(12rem, 42vh));
  }
}

@layer layout {
  .landing-header {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0 var(--s3);
    min-height: var(--landing-header-min);
    padding: var(--s2) var(--s3);
    border-bottom: 1px solid var(--border);
    background: var(--surface);
  }

  .landing-actions {
    order: 2;
    display: flex;
    align-items: center;
    gap: var(--s1);
  }

  .landing-nav {
    order: 3;
    flex: 1 0 100%;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
  }

  .landing-nav a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--s1);
    color: var(--text);
    font-size: 1rem;
    text-decoration: none;
    white-space: nowrap;
  }

  @media (hover: hover) {
    .landing-nav a:hover {
      text-decoration: underline;
    }
  }

  /* A long word breaks instead of widening its grid track: with a larger root font in a wide device
     font, the unbreakable words of the headings, the steps and the lists were wider than a 320px
     screen and the whole page scrolled sideways. `anywhere` (not `break-word`) also lowers the
     min-content size the grids' auto tracks take from their children. */
  .page-landing main {
    max-width: 72rem;
    overflow-wrap: anywhere;
    display: grid;
    gap: var(--s7);
    align-content: start;
  }

  .hero {
    display: grid;
    gap: var(--s6);
    align-items: center;
  }

  .hero-copy {
    display: grid;
    gap: var(--s4);
    justify-items: start;
  }

  .hero-copy p {
    margin: 0;
  }

  .landing-section {
    display: grid;
    gap: var(--s4);
    align-content: start;
  }

  .landing-section > p,
  .landing-section > ul,
  .landing-section > ol {
    margin: 0;
  }
}

@layer components {
  .landing-header .brand {
    flex: 0 1 auto;
    margin-right: auto;
    font-size: 1.0625rem;
  }

  .landing-actions .btn {
    min-width: var(--tap);
    padding: 0 var(--s2);
  }

  .btn-large {
    min-height: calc(var(--tap) + var(--s2));
    padding: 0 var(--s5);
    font-size: 1.0625rem;
  }

  .hero-lead {
    font-size: var(--step-1);
    color: var(--text-muted);
  }

  .demo {
    min-width: 0;
    display: grid;
    gap: var(--s3);
    align-content: start;
    padding: var(--s4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-2);
    min-height: 28rem;
  }

  .demo-hint {
    margin: 0;
    font-size: var(--step--1);
  }

  /* Two groups of two tabs, each a column: the age range above a row of tabs. A group's basis is
     11rem, not its content: a content-sized basis would add the caption to the tabs' width and the
     groups would never share a row. They sit side by side while 2 x 11rem fits (sharing the extra
     width, the tabs wrapping inside a row) and wrap under each other on a narrow screen or a large
     root font. */
  .demo-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3);
  }

  .demo-chip-group {
    flex: 1 1 11rem;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: var(--s2);
  }

  .demo-chip-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
  }

  .demo-chip-range {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--step--1);
  }

  .demo-chip {
    min-height: var(--tap);
    padding: 0 var(--s4);
    border: 2px solid transparent;
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    color: var(--text);
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition:
      background-color var(--dur-1) var(--ease),
      box-shadow var(--dur-1) var(--ease),
      translate var(--dur-1) var(--ease),
      scale var(--dur-1) var(--ease);
  }

  @media (hover: hover) {
    .demo-chip:hover {
      background: var(--accent-soft);
    }
  }

  .demo-chip[aria-pressed="true"] {
    border: 2px solid var(--accent);
    background: var(--surface);
    box-shadow: var(--shadow-1);
  }

  .demo-stage,
  .demo-live,
  .demo-example {
    display: grid;
    gap: var(--s3);
  }

  /* landing.js writes the height of the tallest state to --demo-live-h on the demo: an example, or
     its question with the typing indicator under it. The stage holds the live area and the
     indicator, so the answer grows inside a box that already has its final height. */
  .demo-stage {
    position: relative;
    align-content: start;
    min-height: var(--demo-live-h, 0px);
  }

  /* landing.js lays the examples out here to measure them: as wide as the live area, never shown. */
  .demo-measure {
    position: absolute;
    inset-inline: 0;
    top: 0;
    visibility: hidden;
    pointer-events: none;
  }

  #demo-static {
    display: grid;
    gap: var(--s5);
  }

  #demo-static .demo-example + .demo-example {
    padding-top: var(--s5);
    border-top: 1px solid var(--border);
  }

  .demo-who {
    justify-self: start;
    margin: 0;
    padding: var(--s1) var(--s3);
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: var(--step--1);
  }

  .demo .thread {
    padding: 0;
    gap: var(--s4);
  }

  .demo .typing {
    padding: var(--s1) 0;
  }

  .page-landing .mascot-cheek {
    fill: var(--accent-soft);
  }

  .landing-cards,
  .landing-cols {
    display: grid;
    gap: var(--s4);
    grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  }

  .landing-card {
    display: grid;
    gap: var(--s2);
    align-content: start;
    justify-items: start;
    padding: var(--s5);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-1);
  }

  .landing-card h3 {
    margin: 0;
    font-size: var(--step-1);
    line-height: 1.2;
  }

  .landing-card > svg {
    width: 2.5rem;
    height: 2.5rem;
  }

  .landing-card p {
    margin: 0;
  }

  .landing-card .icon-fill {
    fill: var(--accent);
  }

  .landing-card .icon-line {
    fill: none;
    stroke: var(--on-accent);
    stroke-linecap: round;
    stroke-width: 1.5;
  }

  .landing-band {
    padding: var(--s5);
    border-radius: var(--radius-lg);
    background: var(--surface-2);
  }

  .steps {
    display: grid;
    gap: var(--s2);
    margin: 0;
    padding-left: var(--s5);
  }

  .safety-steps {
    display: grid;
    gap: var(--s3);
    margin: 0;
    padding: 0;
    list-style: none;
    counter-reset: step;
  }

  .safety-steps li {
    display: grid;
    grid-template-columns: 2rem minmax(0, 1fr);
    align-items: start;
    gap: var(--s3);
    counter-increment: step;
  }

  .safety-steps li::before {
    content: counter(step);
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    background: var(--accent);
    color: var(--on-accent);
    font-size: 1rem;
    font-weight: 700;
  }

  .privacy-points {
    display: grid;
    gap: var(--s2);
    padding-left: var(--s5);
  }

  .landing-cta {
    justify-items: center;
    padding: var(--s6) var(--s4);
    border-radius: var(--radius-lg);
    background: var(--accent-soft);
    text-align: center;
  }

  .landing-cta > p {
    margin: 0;
  }
}

@media (min-width: 40rem) {
  .page-landing h1 {
    font-size: var(--step-3);
  }

  .demo {
    padding: var(--s5);
  }

  .landing-band {
    padding: var(--s6);
  }

  .landing-cta {
    padding: var(--s7) var(--s5);
  }
}

@media (min-width: 40rem) {
  @layer components {
    .landing-header .brand {
      font-size: var(--step-1);
    }

    .landing-actions .btn {
      padding: 0 var(--s4);
    }
  }
}

/* One header row needs about 775px in French for a pending account (brand, three nav labels, the
   toggle and the wide call to action); below 49rem the header keeps its two rows. The nav keeps
   its flex-wrap in both layouts: with a larger root font the labels no longer fit in one row (in
   French from 49rem up to about 834px at 125%, and below 49rem at 320px), and a wrapped nav makes
   the header taller instead of wider than the screen. */
@media (min-width: 49rem) {
  html:has(body.page-landing) {
    --landing-header-min: 4.5rem;
  }

  .landing-header {
    flex-wrap: nowrap;
    gap: var(--s4);
    padding: var(--s2) var(--s4);
  }

  .landing-nav a {
    padding: 0 var(--s2);
  }

  .landing-nav {
    order: 0;
    flex: 1 1 auto;
    justify-content: center;
    gap: var(--s2);
  }

  .landing-actions {
    order: 0;
    gap: var(--s2);
  }

  @layer components {
    .landing-header .brand {
      flex: 0 0 auto;
    }
  }
}

@media (min-width: 56rem) {
  .hero {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    align-items: start;
    gap: var(--s7);
  }
}

/* Motion and hover. Two families, keyed on shared hooks so a new control inherits the effect.
   Raised controls (every .btn, the idea cards, the demo chips) lift by 1px and gain a shadow;
   flat items (navigation links, the history rows and summary, the table disclosure's summary) only shift to the soft accent.
   Hover needs a hovering pointer, any movement needs no-preference, and both families press down
   on :active (a history row does not: its delete button is the pressed control there). Only
   translate, scale, box-shadow, background-color and opacity change, so nothing shifts the
   layout, and the individual properties compose with a control's own transform instead of
   replacing it. Every colour pair is one tests/test_contrast.py already lists. The last
   components block, so it wins ties inside the layer. */

@layer components {
  :is(.app-nav, .admin-nav, .window-nav, .section-nav, .pager, .landing-nav) a,
  #range-switch:not(.has-indicator) a,
  .history-about summary,
  .table-details > summary {
    transition:
      background-color var(--dur-1) var(--ease),
      scale var(--dur-1) var(--ease);
  }

  .history-row {
    transition: background-color var(--dur-1) var(--ease);
  }

  @media (hover: hover) {
    .btn-choice:not([aria-pressed="true"]):hover {
      background: var(--accent-soft);
    }

    :is(.app-nav, .admin-nav, .window-nav, .section-nav, .pager, .landing-nav) a:hover,
    #range-switch:not(.has-indicator) a:not([aria-current]):hover,
    .history-row:hover,
    .history-about summary:hover,
    .table-details > summary:hover {
      background-color: var(--accent-soft);
    }

    /* The delete icon sits on a hovered row (the same soft accent), so its own hover is a lighter
       fill with a strong border: --border-strong against --accent-soft and --surface are pairs
       tests/test_contrast.py lists. */
    .history-delete:hover {
      background: var(--surface);
      border-color: var(--border-strong);
      color: var(--text);
    }
  }

  @media (hover: hover) and (prefers-reduced-motion: no-preference) {
    :is(.btn, .idea, .demo-chip):not(:disabled):hover {
      translate: 0 -1px;
      box-shadow: var(--shadow-1);
    }

    .idea:not(:disabled):hover,
    .choice-group [aria-pressed="true"]:not(:disabled):hover,
    .demo-chip[aria-pressed="true"]:not(:disabled):hover {
      box-shadow: var(--shadow-2);
    }
  }

  @media (prefers-reduced-motion: no-preference) {
    :is(.btn, .idea, .demo-chip):not(:disabled):active,
    :is(.app-nav, .admin-nav, .window-nav, .section-nav, .pager, .landing-nav) a:active,
    #range-switch:not(.has-indicator) a:not([aria-current]):active,
    .range-switch a.is-target,
    .history-about summary:active,
    .table-details > summary:active {
      scale: 0.97;
    }
  }
}
