/* The framework's styles: the tokens, the element styles its components
   rely on, and the classes of the components most pages use. A site that
   uses the framework's components links it with `FrameworkStylesheet`
   (`HtmlPage` does). A feature with classes only its own pages use keeps
   them in its own stylesheet (contacts.css, notifications.css, call.css),
   which its components add to the page themselves.

   All of it is in the `hobproj` layer, so the site's own CSS (not in a
   layer) wins over any rule here, whatever its specificity or order. The
   site changes the look by setting the custom properties on :root below,
   and anything else with a plain rule of its own. No `!important` here:
   inside a layer, it would win over the site's. See "Styles" in
   the framework's README. */
@layer hobproj {
  :root {
    /* One palette: every hue has the same lightness (0.65) and chroma (0.11),
       and the hues are 30° apart, so opposite pairs (+180°) complement each
       other. Neutrals are tinted toward blue (265°). Use each color directly or
       with alpha(from var(--color-x) / N%) for lighter shades on any
       background. 0.11 is the highest chroma every hue has in sRGB. */
    --color-red: oklch(0.65 0.11 25);
    --color-orange: oklch(0.65 0.11 55);
    --color-yellow: oklch(0.65 0.11 85); /* golden at this lightness */
    --color-lime: oklch(0.65 0.11 115);
    --color-green: oklch(0.65 0.11 145);
    --color-teal: oklch(0.65 0.11 175);
    --color-cyan: oklch(0.65 0.11 205);
    --color-sky: oklch(0.65 0.11 235);
    --color-blue: oklch(0.65 0.11 265);
    --color-violet: oklch(0.65 0.11 295);
    --color-purple: oklch(0.65 0.11 325);
    --color-pink: oklch(0.65 0.11 355);
    --color-gray: oklch(0.65 0.015 265);
    --color-black: oklch(0.18 0.015 265);
    --color-white: oklch(0.98 0.005 265);

    color-scheme: light dark;
    --color-bg: light-dark(var(--color-white), var(--color-black));
    /* Layers above the page (dialogs, the select list). Shadows barely show
       on a dark page, so there the layer is lighter than the page instead. */
    --color-surface: light-dark(var(--color-white), oklch(from var(--color-black) 0.24 c h));
    /* Dimming the dark page with the palette black (the page color itself)
       changes nothing, so dark mode dims toward a deeper black. */
    --color-backdrop: light-dark(alpha(from var(--color-black) / 50%), oklch(from var(--color-black) 0.1 c h / 70%));
    --color-text: light-dark(var(--color-black), var(--color-white));
    --color-text-muted: alpha(from var(--color-text) / 65%);
    /* Link text needs 4.5:1. The palette value already has that on the dark
       background; on the light one the same hue is darkened to L 0.5. */
    --color-link: light-dark(oklch(from var(--color-blue) 0.5 c h), var(--color-blue));
    --color-link-visited: light-dark(oklch(from var(--color-purple) 0.5 c h), var(--color-purple));
    /* Text for things that can't be undone (`.danger` buttons and headings):
       the palette red moved darker/lighter, so it keeps 4.5:1 on the default
       and the red button backgrounds in every state, in a dialog too
       (≥ 4.9:1). */
    --color-danger-text: light-dark(oklch(from var(--color-red) 0.45 c h), oklch(from var(--color-red) 0.8 c h));
    --color-focus: var(--color-blue);
    --focus-ring: 2px solid var(--color-focus);
    --radius: 0.5rem;
    /* The site's content column (<main>): its width, and the space on each
       side of it on a narrow screen. The site sets its own if they differ. */
    --page-width: 60rem;
    --page-gutter: 1rem;
    /* Side padding for full-width bars (the page header, banners): their
       background spans the window, and their content lines up with <main>'s
       column. The 100% is the width of the element that uses it, so inside
       a container narrower than the page it works out to the gutter. */
    --full-width-padding: max(var(--page-gutter), (100% - var(--page-width)) / 2 + var(--page-gutter));
    --shadow-raised: 0 4px 6px -1px alpha(from var(--color-black) / 10%), 0 2px 4px -2px alpha(from var(--color-black) / 10%);
    /* For layers that float over the page's content without a backdrop
       (floating alerts), so they read as separate from what's under them.
       Dark mode uses a pure-black shadow: the palette black is the page color
       there. */
    --shadow-floating:
      0 12px 28px -6px light-dark(alpha(from var(--color-black) / 22%), oklch(from var(--color-black) 0 0 h / 60%)),
      0 2px 6px -2px light-dark(alpha(from var(--color-black) / 12%), oklch(from var(--color-black) 0 0 h / 40%));
    /* The window's background. On the root, not <body>: containment on the
       body (a `container-type`, like the site's) stops the body's background
       from reaching the window, so it would end where the body's content
       ends. */
    background: var(--color-bg);

    /* Chrome on macOS measures `ch` a few percent too wide for system-ui
       (10.08px for a 9.77px "0" at 16px; Safari matches), so every `ch`
       width in this file comes out a little wider there. */
    font:
      100%/1.5 system-ui,
      sans-serif;
  }

  @keyframes spin {
    100% {
      transform: rotate(360deg);
    }
  }

  @keyframes flash-in {
    from {
      transform: translateY(-0.5em);
      opacity: 0;
    }
  }

  /* `display: none` at the end takes the alert out of the layout, so it
     doesn't leave an empty gap in `.floating-alerts`. */
  @keyframes flash-out {
    to {
      display: none;
      opacity: 0;
    }
  }

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

  body {
    margin: 0;
    color: var(--color-text);
  }

  p,
  li {
    text-wrap: pretty;
  }

  /* Short enough lines to read comfortably. On the list, not each <li>, so
     the bullets count toward it and lists end where paragraphs do. Lines are
     about two characters longer in Chrome (see the `font` on :root). */
  p,
  ul,
  ol {
    max-inline-size: 65ch;
  }

  pre,
  code {
    font-size: 0.9em;
    font-family: ui-monospace, "SF Mono", Menlo, monospace;
  }

  pre {
    border-radius: var(--radius);
    background: alpha(from var(--color-gray) / 12%);
    padding: 0.75em 1em;
    overflow-x: auto;
    code {
      font-size: inherit;
    }
  }

  ::selection {
    background: alpha(from var(--color-blue) / 30%);
  }

  noscript {
    display: block;
    margin-block: 1rem;
  }

  /* The h1–h6 fallbacks are for browsers without `:heading` (Chrome 154).
     `:is()` skips a selector it doesn't know instead of dropping the whole
     rule; remove the fallbacks once `:heading` is supported everywhere. */
  :is(:heading, h1, h2, h3, h4, h5, h6) {
    margin-block: 1.75em 0.5em;
    font-weight: 650;
    line-height: 1.2;
    letter-spacing: -0.01em;
    text-wrap: balance;
  }

  :is(:heading(1), h1) {
    margin-block-start: 0;
    font-size: 2rem;
    letter-spacing: -0.02em;
  }

  :is(:heading(2), h2) {
    font-size: 1.5rem;
  }

  :is(:heading(3), h3) {
    font-size: 1.25rem;
  }

  :is(:heading(4, 5, 6), h4, h5, h6) {
    font-size: 1rem;
  }

  /* Marks a section whose actions can't be undone, like "Delete account". */
  :is(:heading, h1, h2, h3, h4, h5, h6).danger {
    color: var(--color-danger-text);
  }

  /* Not a link that looks like a button (`a.button`, with the button styles
     below). `:where()` keeps this at the specificity of a plain `a`. */
  a:where(:not(.button)) {
    color: var(--color-link);
    text-decoration-color: alpha(from var(--color-link) / 40%);
    text-underline-offset: 0.15em;
    /* Set directly: browsers ignore custom properties changed in `:visited`. */
    &:visited {
      color: var(--color-link-visited);
      text-decoration-color: alpha(from var(--color-link-visited) / 40%);
    }
    &:hover {
      text-decoration-color: currentColor;
      text-decoration-thickness: 2px;
    }
    &:active {
      opacity: 0.7;
    }
    &:focus-visible {
      outline: var(--focus-ring);
      outline-offset: 2px;
      border-radius: 2px;
    }
  }

  /* The states are wrapped in `:where()` so they add no specificity, which
     lets the one-class `.close` reset below win over all of them. The states
     read only the `--button-*` variables, so a variant sets those and every
     state follows. A link that goes to another page but should look like a
     button (`<a class="button primary">`) takes the same styles; `:where()`
     keeps every rule here at the specificity of a plain `button`. */
  button,
  :where(a.button) {
    --button-color: var(--color-gray);
    --button-text: var(--color-text);
    /* In light mode the default button has no fill of its own, only a light
       border and a soft shadow below, so it sits on any background (page,
       dialog, alert); hovering and pressing tint it. On a dark background a
       border and shadow barely show, so there it has a gray fill instead and
       no border or shadow. */
    --button-bg: light-dark(transparent, alpha(from var(--button-color) / 22%));
    --button-bg-hover: light-dark(alpha(from var(--button-color) / 8%), alpha(from var(--button-color) / 28%));
    --button-bg-active: light-dark(alpha(from var(--button-color) / 14%), alpha(from var(--button-color) / 34%));
    --button-border: light-dark(alpha(from var(--button-color) / 20%), transparent);
    --button-border-hover: light-dark(alpha(from var(--button-color) / 30%), transparent);
    /* The default button's shadow, in light mode only; `secondary` keeps it,
       `primary` and `danger-strong` turn it off. `light-dark()`
       takes only colors (or images), not a whole shadow, so dark mode gets a
       transparent one. */
    --button-shadow: 0 1px 3px light-dark(alpha(from var(--color-black) / 8%), transparent);
    transition:
      background-color 0.15s,
      border-color 0.15s,
      box-shadow 0.15s;
    box-shadow: var(--button-shadow);
    border: 1px solid var(--button-border);
    border-radius: var(--radius);
    background: var(--button-bg);
    padding: 0.45em 0.9em;
    color: var(--button-text);
    font: inherit;
    line-height: 1.2;
    &:where(:hover:not(:disabled)) {
      border-color: var(--button-border-hover);
      background: var(--button-bg-hover);
    }
    /* Pressed into the page: the shadow goes as the button moves down. */
    &:where(:active:not(:disabled)) {
      translate: 0 1px;
      box-shadow: none;
      background: var(--button-bg-active);
    }
    /* The main action of a page or dialog; at most one per screen. The fill
       is the palette blue at L 0.5 (light) or as is (dark), and hover/active
       move it away from the text color, so contrast only grows (≥ 5.7:1). */
    &.primary {
      --button-text: light-dark(var(--color-white), var(--color-black));
      --button-bg: light-dark(oklch(from var(--color-blue) 0.5 c h), var(--color-blue));
      --button-bg-hover: light-dark(oklch(from var(--color-blue) 0.45 c h), oklch(from var(--color-blue) 0.7 c h));
      --button-bg-active: light-dark(oklch(from var(--color-blue) 0.4 c h), oklch(from var(--color-blue) 0.75 c h));
      --button-border: transparent;
      --button-border-hover: transparent;
      --button-shadow: none;
      font-weight: 500;
    }
    /* A tinted fill and border at rest, darker on hover and when pressed, in
       the button's color (Apple's "tinted" style): blue for `secondary`, red
       for `danger-strong`. The text is that color moved darker (light) or
       lighter (dark), as `--color-danger-text` is: at least 5.1:1 in every
       state, on the page and in a dialog (computed with culori's WCAG
       contrast over the composited fill). */
    &:is(.secondary, .danger-strong) {
      --button-bg: light-dark(alpha(from var(--button-color) / 12%), alpha(from var(--button-color) / 18%));
      --button-bg-hover: light-dark(alpha(from var(--button-color) / 20%), alpha(from var(--button-color) / 26%));
      --button-bg-active: light-dark(alpha(from var(--button-color) / 28%), alpha(from var(--button-color) / 34%));
      --button-border: alpha(from var(--button-color) / 35%);
      --button-border-hover: alpha(from var(--button-color) / 50%);
    }
    /* For actions that delete something for good: red text. `danger-strong`,
       for the final, irreversible step, also has the red tint and border. */
    &:is(.danger, .danger-strong) {
      --button-text: var(--color-danger-text);
    }
    /* No shadow: the final step stays flat. `secondary` keeps the default
       button's (light mode only), so it sits on the page like the buttons
       beside it. */
    &.danger-strong {
      --button-color: var(--color-red);
      --button-shadow: none;
    }
    &.secondary {
      --button-color: var(--color-blue);
      --button-text: light-dark(oklch(from var(--color-blue) 0.45 c h), oklch(from var(--color-blue) 0.8 c h));
    }
    /* For buttons that sit inside another component (table row actions, a
       button in an alert) rather than being the page's or dialog's own
       action. About 27px tall, above WCAG 2.5.8's 24px minimum target. */
    &.small {
      padding: 0.3em 0.7em;
      font-size: 0.875em;
    }
    /* A link doesn't get the browser's button layout or lose its underline
       on its own. */
    &:where(a) {
      display: inline-block;
      text-decoration-line: none;
    }
    &:where(:focus-visible) {
      outline: var(--focus-ring);
      outline-offset: 2px;
    }
    /* The hand shows what can be clicked; a disabled (or busy) button keeps
       the arrow. */
    &:where(:not(:disabled)) {
      cursor: pointer;
    }
    &:where(:disabled) {
      opacity: 0.5;
    }
    &:where(.busy) {
      display: inline-flex;
      justify-content: center;
      align-items: center;
      gap: 0.5em;
      opacity: 0.8;
      &::before {
        display: inline-block;
        animation: spin 0.8s linear infinite;
        border: 2px solid alpha(from var(--button-text) / 30%);
        border-top-color: var(--button-text);
        border-radius: 50%;
        width: 0.9em;
        height: 0.9em;
        content: "";
      }
    }
    /* `all: unset` drops every button style, the cursor and focus outline
       included, so the close button's own styles are set again after it. They
       sit in a nested block because a declaration sorter would move `all` below
       them in the same block, where it would undo them. */
    &.close {
      all: unset;
      & {
        opacity: 0.8;
        transition:
          opacity 0.15s,
          background-color 0.15s;
        cursor: pointer;
        margin: -0.5em;
        border-radius: var(--radius);
        /* Trim the line box to the x's own ink (x-height to baseline) so
           centering the button centers the glyph; the padding and negative
           margin give it a click area of about 28px (WCAG 2.5.8 asks for 24)
           without shifting the layout. */
        padding: 0.5em;
        font: 1.1rem/1 monospace;
        text-box: trim-both ex alphabetic;
      }
      &:hover {
        opacity: 1;
        background: alpha(from var(--color-text) / 10%);
      }
      &:active {
        background: alpha(from var(--color-text) / 16%);
      }
      /* `all: unset` removes the browser's focus outline too. */
      &:focus-visible {
        outline: var(--focus-ring);
        outline-offset: 2px;
      }
    }
  }

  label {
    display: block;
    margin-block-end: 0.3em;
    font-weight: 500;
  }

  :is(
    input:not(
      [type="checkbox"],
      [type="radio"],
      [type="range"],
      [type="color"],
      [type="file"],
      [type="hidden"],
      [type="submit"],
      [type="reset"],
      [type="button"],
      [type="image"]
    ),
    textarea,
    select
  ) {
    /* Also used by the input height below. */
    --field-padding-block: 0.45em;
    --field-border-width: 1px;
    transition: border-color 0.15s;
    /* Solid, not alpha: a field's outline needs 3:1 against the page. */
    border: var(--field-border-width) solid var(--color-gray);
    border-radius: var(--radius);
    background: var(--color-bg);
    padding: var(--field-padding-block) 0.7em;
    max-inline-size: 100%;
    color: var(--color-text);
    font: inherit;
    line-height: 1.2;
    /* A fixed height (one line, plus the padding and border on both sides),
       so the field keeps its size when Safari adds its AutoFill key button on
       focus. That button is taller than the line and would make the field
       grow; with a fixed height it fits in the padding instead. */
    &:where(input) {
      block-size: calc(1lh + 2 * var(--field-padding-block) + 2 * var(--field-border-width));
    }
    &:hover:not(:disabled, :focus) {
      border-color: var(--color-text);
    }
    &:focus-visible {
      outline: var(--focus-ring);
      outline-offset: 1px;
      border-color: var(--color-focus);
    }
    /* Unlike `:invalid`, only after the user has edited the field. */
    &:user-invalid {
      border-color: var(--color-red);
      &:focus-visible {
        outline-color: var(--color-red);
      }
    }
    &:read-only:not(select) {
      background: alpha(from var(--color-gray) / 10%);
    }
    &:disabled {
      opacity: 0.5;
      background: alpha(from var(--color-gray) / 10%);
    }
    &::placeholder {
      color: var(--color-text-muted);
    }
    /* An inset shadow is the usual way to cover the browser's autofill
       background, which can't be overridden with `background`. */
    &:autofill {
      box-shadow: inset 0 0 0 100vmax alpha(from var(--color-yellow) / 20%);
    }
  }

  textarea {
    /* `field-sizing` would shrink the width to the placeholder too. */
    min-inline-size: min(100%, 24ch);
    min-block-size: 4lh;
    field-sizing: content;
    resize: vertical;
  }

  /* `base-select` lets the select and its option list take the styles above;
     the native look ignores most of them. */
  select {
    &,
    &::picker(select) {
      appearance: base-select;
    }
    &::picker-icon {
      color: var(--color-text-muted);
    }
    &::picker(select) {
      margin-block: 0.25em;
      box-shadow: var(--shadow-raised);
      border: 1px solid alpha(from var(--color-gray) / 35%);
      border-radius: var(--radius);
      background: var(--color-surface);
      padding: 0.25em;
      color: var(--color-text);
    }
    option {
      border-radius: calc(var(--radius) - 0.25em);
      padding: 0.35em 0.6em;
      &:hover,
      &:focus-visible {
        background: alpha(from var(--color-gray) / 15%);
      }
      &:focus-visible {
        outline: var(--focus-ring);
        outline-offset: -2px;
      }
    }
  }

  /* The hand shows what can be clicked, as on buttons: a checkbox or a radio
     button (the browser gives it the arrow), and the label around one. A
     disabled one keeps the arrow. */
  :is(input[type="checkbox"], input[type="radio"]):not(:disabled),
  label:has(> :is(input[type="checkbox"], input[type="radio"]):not(:disabled)) {
    cursor: pointer;
  }

  :is(input[type="checkbox"], input[type="radio"], input[type="range"]),
  progress {
    accent-color: var(--color-blue);
    &:focus-visible {
      outline: var(--focus-ring);
      outline-offset: 2px;
    }
  }

  fieldset {
    border: 1px solid alpha(from var(--color-gray) / 35%);
    border-radius: var(--radius);
  }

  legend {
    padding-inline: 0.3em;
    font-weight: 600;
  }

  table,
  .scrollable-table {
    --table-border-color: alpha(from var(--color-gray) / 30%);
  }

  table {
    border: 1px solid var(--table-border-color);
    border-radius: var(--radius);
    border-spacing: 0;
    /* Clips the header tint to the rounded corners. */
    overflow: clip;
    /* Dates, times and IP addresses line up from row to row. */
    font-variant-numeric: tabular-nums;
    .actions {
      display: flex;
      gap: 0.3em;
    }
  }

  /* A table that scrolls sideways inside its frame when it's wider than its
     container (ScrollableTable). The frame (border and rounded corners) moves
     from the table to the wrapper, so it stays still while the rows scroll,
     and the wrapper clips the header tint to the corners instead. As wide as
     the table, but never wider than its container. */
  .scrollable-table {
    /* Scroll shadows: a soft shadow on a side where content is hidden. The
       shadows (`scroll`) stay at the frame's edges; the covers (`local`) scroll
       with the content and hide a shadow once that end is reached. The covers
       match the page behind the frame. */
    --scroll-cover: var(--color-bg);
    --scroll-shadow: light-dark(alpha(from var(--color-black) / 15%), oklch(from var(--color-black) 0 0 h / 60%));
    border: 1px solid var(--table-border-color);
    border-radius: var(--radius);
    background:
      linear-gradient(to right, var(--scroll-cover) 40%, transparent) left / 3rem 100% no-repeat local,
      linear-gradient(to left, var(--scroll-cover) 40%, transparent) right / 3rem 100% no-repeat local,
      radial-gradient(farthest-side at 0 50%, var(--scroll-shadow), transparent) left / 1rem 100% no-repeat scroll,
      radial-gradient(farthest-side at 100% 50%, var(--scroll-shadow), transparent) right / 1rem 100% no-repeat scroll var(--scroll-cover);
    inline-size: fit-content;
    max-inline-size: 100%;
    overflow-x: auto;
    &:focus-visible {
      outline: var(--focus-ring);
      outline-offset: 2px;
    }
    > table {
      border: 0;
      border-radius: 0;
    }
    /* Values that read badly when split ("Sep 26, 1:38 / PM"). When space runs
       short, longer text (a device, a place) wraps at its spaces instead. */
    :is(time, button) {
      white-space: nowrap;
    }
  }

  caption {
    padding-block-end: 0.5em;
    font-weight: 600;
    text-align: start;
  }

  td,
  th {
    /* rem, not em: the smaller header text would get less padding and no
       longer line up with the cells below it. */
    padding: 0.6rem 0.9rem;
    text-align: start;
  }

  th {
    background: alpha(from var(--color-gray) / 10%);
    color: var(--color-text-muted);
    font-weight: 600;
    font-size: 0.875em;
    white-space: nowrap;
  }

  /* Lines between rows only, so none doubles up with the outer border. */
  thead th,
  tbody tr:not(:last-child) > * {
    border-block-end: 1px solid var(--table-border-color);
  }

  tbody tr {
    transition: background-color 0.15s;
    &:hover,
    &:focus-within {
      background: alpha(from var(--color-gray) / 6%);
    }
  }

  /* One width for every dialog, set here rather than by the longest text in
     it, so a short form doesn't spread out. The browser's own `max-width`
     for a modal dialog keeps it inside a phone's screen. */
  dialog {
    --dialog-padding: 1.25rem;
    --dialog-radius: 0.75rem;
    box-shadow: var(--shadow-raised);
    border: 1px solid alpha(from var(--color-gray) / 30%);
    border-radius: var(--dialog-radius);
    background: var(--color-surface);
    padding: var(--dialog-padding);
    inline-size: 26rem;
    color: var(--color-text);
    &::backdrop {
      background: var(--color-backdrop);
    }
    > :is(:heading(2), h2) {
      margin-block-start: 0;
      font-size: 1.25rem;
    }
    /* Space alone sets the buttons apart; the dialogs are short. 2rem is
       twice the gap between paragraphs and more than the dialog's padding, so
       the buttons read as their own group. */
    > .actions {
      display: flex;
      flex-wrap: wrap;
      justify-content: end;
      /* Not the default `stretch`, which would make a small button as tall as
         its full-size neighbour. */
      align-items: center;
      gap: 0.5rem;
      margin-block-start: 2rem;
    }
    /* For long dialogs: the buttons get a bar along the bottom edge that stays
       in view while the content scrolls under it. The negative margins cancel
       the dialog's padding, and the bottom corners follow the dialog's (minus
       its 1px border). The tint sits on the surface color so the bar is
       opaque over the scrolling content. */
    &.sticky-actions > .actions {
      --actions-tint: alpha(from var(--color-gray) / 8%);
      position: sticky;
      bottom: calc(-1 * var(--dialog-padding));
      margin: 1.5rem calc(-1 * var(--dialog-padding)) calc(-1 * var(--dialog-padding));
      border-block-start: 1px solid alpha(from var(--color-gray) / 20%);
      border-end-start-radius: calc(var(--dialog-radius) - 1px);
      border-end-end-radius: calc(var(--dialog-radius) - 1px);
      background: linear-gradient(var(--actions-tint), var(--actions-tint)) var(--color-surface);
      padding: 0.9rem var(--dialog-padding);
    }
  }

  /* On a phone the buttons share the row instead of hugging the right edge. */
  @media (width < 30rem) {
    dialog > .actions > * {
      flex: 1;
    }
  }

  /* An alert, anywhere on the page: in the page flow, as wide as its
     container, rounded. `.banner` makes it square, with lines only on top and
     bottom; `.floating-alerts` below makes it float. Alerts are dialogs, so
     this also replaces the dialog styles (a <dialog> is `absolute` by
     default, and gets the dialog shadow). */
  .alert {
    /* Each type sets only `--alert-color`; the tint, border and badge all
       derive from it. The tint sits on a solid base so nothing under a
       floating alert shows through. */
    --alert-color: var(--color-gray);
    --alert-tint: light-dark(alpha(from var(--alert-color) / 16%), alpha(from var(--alert-color) / 24%));
    display: flex;
    position: static;
    /* The icon, actions and close button line up with the message's first
       line (its baseline), not the middle of a message that wraps. */
    align-items: baseline;
    gap: 0.9em;
    margin: 0;
    box-shadow: none;
    border: 1px solid alpha(from var(--alert-color) / 55%);
    border-radius: 0.75rem;
    background: linear-gradient(var(--alert-tint), var(--alert-tint)) var(--color-surface);
    padding: 0.9em 1.1em;
    inline-size: auto;
    color: var(--color-text);
    /* `display: flex` above would show a closed one too. */
    &:not(:open) {
      display: none;
    }

    &::before {
      /* A block container, not flex, so `text-box` below can trim it. */
      display: inline-block;
      flex-shrink: 0;
      align-content: center;
      /* Centered on the message's first line (see the margin above), not
         aligned by baseline like the rest: the baseline depends on the glyph,
         and the × is trimmed to its x-height below, which left the danger
         badge lower than the others. */
      align-self: flex-start;
      /* Half the difference between the message's first line (line-height
         1.5 of the message's font, which is 1 / 0.9 of this one) and the
         circle puts the circle's center on the middle of the capitals. Mostly
         lowercase text looks centered lower, between the middles of its
         capitals and of its lowercase letters, so the circle goes a quarter
         of the cap-height/x-height gap lower. */
      margin-block-start: calc((1.5em / 0.9 - 1.5em) / 2 + (1cap - 1ex) / 4);
      border-radius: 50%;
      background: var(--alert-color);
      width: 1.5em;
      height: 1.5em;
      /* Every hue has the same lightness, so black reads the same on all. */
      color: var(--color-black);
      font: 700 0.9em system-ui;
      text-align: center;
    }
    > .message {
      flex: 1;
    }
    /* Buttons for the alert (e.g. "Sign in again"), before the close
       button. Alerts are dialogs, so this also resets the dialog footer's
       spacing from `dialog > .actions`. */
    > .actions {
      display: flex;
      gap: 0.5em;
      margin: 0;
    }
    &.info {
      --alert-color: var(--color-blue);
      &::before {
        content: "i";
        font-style: italic;
      }
    }
    &.success {
      --alert-color: var(--color-green);
      &::before {
        content: "✓";
      }
    }
    &.warning {
      --alert-color: var(--color-yellow);
      &::before {
        content: "!";
      }
    }
    &.danger {
      --alert-color: var(--color-red);
      &::before {
        content: "×";
        /* × (U+00D7), not ✕ (U+2715): system fonts have ×, while ✕ comes
           from a fallback font that differs per browser, so the trim below
           (which uses system-ui's measurements) left it off-center, high in
           Chrome. × sits in the x-height band, so centering the full line
           box leaves it low; trim to that band. */
        text-box: trim-both ex alphabetic;
      }
    }

    /* For a notice about the whole page (e.g. the session expiring), at the
       top of <body>: the tint spans the window, and the text lines up with
       <main>'s content column. */
    &.banner {
      border-radius: 0;
      border-inline: 0;
      padding-inline: var(--full-width-padding);
    }
  }

  /* Alerts that float at the top of the window: the page's container (alerts
     from showAlert() and the flash message), and the one showAlert() adds
     inside an open modal dialog. The latter is a popover (to show above the
     dialog), so this also resets the browser's popover styles. Only here do
     alerts get a shadow and animations. */
  .floating-alerts {
    display: flex;
    position: fixed;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    z-index: 1;
    margin: 0;
    inset: 1rem 1rem auto;
    border: 0;
    background: none;
    padding: 0;
    width: auto;
    height: auto;
    overflow: visible;
    pointer-events: none;
    color: inherit;
    > .alert {
      animation: flash-in 0.2s ease-out;
      box-shadow: var(--shadow-floating);
      inline-size: fit-content;
      max-inline-size: min(50em, 100%);
      pointer-events: auto;
      &.auto-hide {
        animation:
          flash-in 0.2s ease-out,
          flash-out 0.3s ease-in 5s forwards;
        /* Lets the user keep the message on screen while reading it. */
        &:hover {
          animation-play-state: paused;
        }
      }
    }
  }

  /* Text led by a small icon, e.g. a passkey's provider. */
  .icon-label {
    white-space: nowrap;
    img {
      vertical-align: text-bottom;
      margin-inline-end: 0.4em;
      height: 1.2em;
      /* Otherwise the `width` attribute keeps it at 18px while the height
         grows with the font, stretching the icon. */
      inline-size: auto;
    }
  }

  /* A text field in a dialog starts at 25ch and grows with what's typed, up
     to the field's length limit: every name field the same width (a
     username, a passkey name, a website's own). The sign-up form's username
     too, though it's on a page. The width is in `ch`, since the `size`
     attribute would give each browser a different width. The fields fit
     about one more character in Chrome (see the `font` on :root). */
  :is(dialog, .signup-form)
    :is(input:not([type]), input:is([type="text"], [type="search"], [type="email"], [type="url"], [type="tel"], [type="password"])) {
    min-inline-size: 25ch;
    field-sizing: content;
  }
}
