/*! leicht v0.2.0 — classless-first CSS, modern syntax only. MIT */
@layer leicht.base, leicht.components, leicht.utilities;
@layer leicht.base, leicht.components, leicht.utilities;

@layer leicht.base {
  :root {
    color-scheme: light dark;
    --l-font: Geist, "Inter Variable", Inter, system-ui, sans-serif;
    --l-mono: "Geist Mono", "JetBrains Mono", ui-monospace, monospace;
    --l-width: 72rem;
    --l-aside: 15rem;
    --l-table-max: none;
    --l-space: 1rem;
    /* One knob. Layout spacing derives from it, so --l-space: .75rem actually compacts the whole
       UI. Component interiors stay in em instead, so they scale with their own font size — which
       is what lets .sm/.lg resize a control with a single declaration. */
    --l-space-xs: calc(var(--l-space) * .25);
    --l-space-sm: calc(var(--l-space) * .5);
    --l-space-md: calc(var(--l-space) * .75);
    --l-space-lg: calc(var(--l-space) * 1.5);
    --l-space-xl: calc(var(--l-space) * 2);
    --l-radius: .5rem;
    --l-hue: 257;

    /* near-neutral zinc greys with a whisper of the hue; near-black dark mode */
    --l-bg: light-dark(oklch(99.2% .002 var(--l-hue)), oklch(14.5% .006 var(--l-hue)));
    --l-fg: light-dark(oklch(21% .012 var(--l-hue)), oklch(96.5% .004 var(--l-hue)));
    /* macOS system blue, darkened just enough for AA text; lighter in the dark */
    --l-primary: light-dark(oklch(55% .2 var(--l-hue)), oklch(72% .15 var(--l-hue)));
    /* same lightness as primary, so --l-on-primary stays legible on each */
    --l-error: light-dark(oklch(56% .2 29), oklch(72% .16 29));
    --l-success: light-dark(oklch(52% .14 148), oklch(76% .17 148));
    /* amber at the same lightness as the other states, so --l-on-primary needs no exception:
       5.04:1 light and 10.43:1 dark, between --l-error (5.03) and --l-success (5.06). A brighter
       amber would read better and fail AA on its own fill. [see test/contrast.test.mjs] */
    --l-warning: light-dark(oklch(54% .13 75), oklch(80% .15 80));

    --l-on-primary: var(--l-bg);
    --l-surface: color-mix(in oklab, var(--l-fg) 4%, var(--l-bg));
    --l-muted: color-mix(in oklab, var(--l-fg) 64%, var(--l-bg));
    --l-border: color-mix(in oklab, var(--l-fg) 12%, #0000);
    /* a token so a skin can re-point the focus ring instead of restating the rule */
    --l-focus: 0 0 0 3px color-mix(in oklab, var(--l-primary) 25%, #0000);
    --l-shadow: 0 6px 20px -10px #0003;

    /* macOS-style vibrancy: translucent fill, lit top edge, faint rim */
    --l-glass: color-mix(in oklab, var(--l-bg) 62%, #0000);
    /* WebKit does not apply backdrop-filter in the top layer, so dialogs, menus and the select
       picker need a material that is legible with no blur behind it. Derived from --l-glass, so
       a skin that makes the glass solid gets a solid top layer too. [measured, WebKit 26.5] */
    --l-glass-top: color-mix(in oklab, var(--l-bg) 90%, var(--l-glass));
    --l-glass-edge: inset 0 1px light-dark(#fffd, #fff4), inset 0 0 0 1px light-dark(#fff8, #fff1);
    --l-blur: blur(20px) saturate(1.8);
    --l-corner: squircle;
    /* liquid glass. The refraction layer carries its SVG filter in a data URI, so it ships
       inside this stylesheet. Swap --l-refract for any filter url() — docs/ has an edge-lens
       variant that bends only near the rim instead of rippling the whole surface. */
    --l-liquid-tint: 22%;
    --l-liquid-blur: 0px;
    --l-liquid-sat: 1.6;
    --l-liquid-edge: light-dark(#fffb, #fff6);
    --l-refract: url("data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter%20id=%22l%22%20x=%220%25%22%20y=%220%25%22%20width=%22100%25%22%20height=%22100%25%22%3E%3CfeTurbulence%20type=%22fractalNoise%22%20baseFrequency=%220.008%200.008%22%20numOctaves=%222%22%20seed=%2292%22%20result=%22n%22/%3E%3CfeGaussianBlur%20in=%22n%22%20stdDeviation=%222%22%20result=%22b%22/%3E%3CfeDisplacementMap%20in=%22SourceGraphic%22%20in2=%22b%22%20scale=%2270%22%20xChannelSelector=%22R%22%20yChannelSelector=%22G%22/%3E%3C/filter%3E%3C/svg%3E#l");
    /* syntax colours for ::highlight(l-*) — bring any tokenizer that fills CSS.highlights */
    --l-syn-1: light-dark(oklch(50% .16 257), oklch(78% .12 240));
    --l-syn-2: light-dark(oklch(50% .13 50), oklch(80% .11 60));
    --l-syn-3: light-dark(oklch(47% .12 150), oklch(80% .12 150));
    /* duocolor icons: give secondary strokes stroke: var(--l-duo) */
    --l-duo: color-mix(in oklab, currentColor 40%, #0000);
  }
  /* a squircle reads tighter than a circle arc of the same radius */
  @supports (corner-shape: squircle) { :root { --l-radius: .8rem } }
  [data-theme=light] { color-scheme: light }
  [data-theme=dark] { color-scheme: dark }
}

/* No :where() needed: the layer, not specificity, decides against your CSS. */
@layer leicht.base {
  *, ::before, ::after { box-sizing: border-box }
  html { -webkit-text-size-adjust: none; text-size-adjust: none; scrollbar-gutter: stable }
  body { margin: 0; min-block-size: 100dvh; background: var(--l-bg); color: var(--l-fg); font: 1rem/1.6 var(--l-font) }

  body > :is(header, main, footer), .container {
    max-inline-size: var(--l-width);
    margin-inline: auto;
    /* was var(--l-space): 1rem top against up to 2rem sides read visibly tight */
    padding: var(--l-space-lg) clamp(1rem, 4vw, 2rem);
  }
  body > main, .container { container-type: inline-size }
  /* both ends, and the same list on both: a heading inside a card's <header> kept its 1.6em
     top margin, so the title sat low in the bar it is centred in */
  :is(body > main, article, .card, dialog, [popover], header, footer) > :first-child { margin-block-start: 0 }
  :is(article, .card, dialog, [popover], header, footer) > :last-child { margin-block-end: 0 }

  h1, h2, h3, h4, h5, h6 { margin-block: 1.6em .5em; line-height: 1.2; text-wrap: balance }
  h1 { font-size: 2.25rem }
  h2 { font-size: 1.75rem }
  h3 { font-size: 1.375rem }
  h4 { font-size: 1.125rem }
  p, ul, ol, dl, pre, blockquote, table, figure, details, fieldset { margin: 0; margin-block-end: var(--l-space) }
  p, li { text-wrap: pretty }
  /* 1.6em is prose rhythm; after a component the gap is already carried by its own margin */
  :is(article, .card, figure, table, pre, details, .row, .alert) + :is(h1, h2, h3, h4, h5, h6) {
    margin-block-start: var(--l-space-lg);
  }
  small, figcaption, thead, body > footer { color: var(--l-muted) }
  figcaption, thead, body > footer { font-size: .875rem }

  a {
    color: var(--l-primary);
    text-underline-offset: .2em;
    &:hover { text-decoration-thickness: 2px }
  }
  :focus-visible { outline: 2px solid var(--l-primary); outline-offset: 2px }

  code, kbd, samp { unicode-bidi: isolate }
  code, kbd, pre { font: .875em/1.6 var(--l-mono); font-variant-ligatures: none; border-radius: calc(var(--l-radius) * .6); background: var(--l-surface) }
  code, kbd { padding: .1em .35em }
  kbd { border: 1px solid var(--l-border); border-block-end-width: 2px }
  pre {
    overflow-x: auto;
    /* code is LTR even in an RTL document; isolate keeps it from reordering the line around it */
    direction: ltr;
    padding: var(--l-space);
    & code { padding: 0; font-size: 1em }
  }
  ::highlight(l-tag), ::highlight(l-at) { color: var(--l-syn-1) }
  ::highlight(l-attr), ::highlight(l-prop) { color: var(--l-syn-2) }
  ::highlight(l-string), ::highlight(l-num) { color: var(--l-syn-3) }
  ::highlight(l-comment), ::highlight(l-punct) { color: var(--l-muted) }
  blockquote { padding-inline-start: var(--l-space); border-inline-start: 3px solid var(--l-primary); color: var(--l-muted) }
  hr { margin-block: var(--l-space-xl); border: 0; border-block-start: 1px solid var(--l-border) }
  mark { padding-inline: .2em; border-radius: .2em; background: color-mix(in oklab, var(--l-primary) 25%, #0000); color: inherit }
  img, video { max-inline-size: 100%; block-size: auto; vertical-align: middle }

  table { inline-size: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums }
  th, td { padding: .6em .75em; border-block-end: 1px solid var(--l-border); text-align: start }

  details {
    padding: var(--l-space-sm) var(--l-space);
    border: 1px solid var(--l-border);
    border-radius: var(--l-radius);
    interpolate-size: allow-keywords;
    & > summary { cursor: pointer; font-weight: 600 }
    &[open] > summary { margin-block-end: var(--l-space-sm) }
    &::details-content { block-size: 0; overflow: clip; transition: block-size .2s, content-visibility .2s allow-discrete }
    &[open]::details-content { block-size: auto }
  }
}

@layer leicht.base {
  button, input, select, textarea { font: inherit; color: inherit }
  input:not([type=checkbox], [type=radio], [type=range], [type=color], [type=submit], [type=button], [type=reset]), select, textarea {
    display: block;
    inline-size: 100%;
    padding: .5em .75em;
    border: 1px solid var(--l-border);
    border-radius: var(--l-radius);
    background: var(--l-bg);
    transition: border-color .15s, box-shadow .15s;
    &:focus { outline: 0; border-color: var(--l-primary); box-shadow: var(--l-focus) }
    &:user-invalid { border-color: var(--l-error) }
  }
  textarea { min-block-size: 5lh; resize: vertical; field-sizing: content }
  input, progress { accent-color: var(--l-primary) }
  [type=checkbox], [type=radio] { inline-size: 1.1em; block-size: 1.1em; margin: 0; margin-inline-end: .4em; vertical-align: -.15em }

  progress, meter {
    appearance: none;
    inline-size: 100%;
    block-size: .55rem;
    border: 0;
    border-radius: 99em;
    background: var(--l-surface);
    /* every engine names the fill differently; none of them inherit the track's radius */
    &::-webkit-progress-bar { border-radius: 99em; background: var(--l-surface) }
    &::-webkit-progress-value { border-radius: 99em; background: var(--l-primary) }
    &::-moz-progress-bar { border-radius: 99em; background: var(--l-primary) }
  }
  meter {
    &::-webkit-meter-bar { border: 0; border-radius: 99em; background: var(--l-surface) }
    &::-webkit-meter-optimum-value { border-radius: 99em; background: var(--l-success) }
    &::-webkit-meter-suboptimum-value { border-radius: 99em; background: var(--l-primary) }
    &::-webkit-meter-even-less-good-value { border-radius: 99em; background: var(--l-error) }
    &::-moz-meter-bar { border-radius: 99em; background: var(--l-success) }
  }

  /* <input type="checkbox" role="switch">: a switch without a class */
  [role=switch] {
    appearance: none;
    flex: none;
    inline-size: 2.25em;
    block-size: 1.3em;
    padding: .15em;
    border: 1px solid var(--l-border);
    border-radius: 99em;
    corner-shape: var(--l-corner);
    background: var(--l-surface);
    transition: background .2s, border-color .2s;
    &::after {
      content: "";
      display: block;
      block-size: 100%;
      aspect-ratio: 1;
      border-radius: 50%;
      background: var(--l-bg);
      box-shadow: 0 1px 2px #0003;
      transition: translate .2s var(--l-ease, cubic-bezier(.2, .7, .3, 1));
    }
    &:checked {
      background: var(--l-primary);
      border-color: #0000;
      &::after { translate: .95em 0 }
      &:dir(rtl)::after { translate: -.95em 0 }
    }
  }
  /* a hint sits under its field, not wedged against the next label */
  :is(input, select, textarea) + small { display: block; margin-block-end: var(--l-space) }

  :disabled { opacity: .5; cursor: not-allowed }

  label {
    display: block;
    margin-block-end: var(--l-space);
    font-weight: 500;
    & > :is(input, select, textarea) { margin-block-start: .3em; font-weight: 400 }
    &:has(> [type=checkbox], > [type=radio]) { font-weight: 400; cursor: pointer }
  }
  /* a <select> must not be wrapped in its <label>: WebKit then focuses it without opening the
     menu, so the separate label + for/id pattern is styled to match the wrapping one */
  label:has(+ :is(input, select, textarea)) { margin-block-end: .3em }
  label + :is(input, select, textarea) { margin-block-end: var(--l-space) }

  fieldset { padding: var(--l-space); border: 1px solid var(--l-border); border-radius: var(--l-radius) }
  legend { padding-inline: .4em; font-weight: 600 }

  button, [type=submit], [type=button], [type=reset], .button {
    --_bg: var(--l-primary);
    --_fg: var(--l-on-primary);
    --_bd: #0000;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: .5em;
    padding: .5em 1.1em;
    border: 1px solid var(--_bd);
    border-radius: var(--l-radius);
    background: var(--_bg);
    color: var(--_fg);
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    transition: filter .15s, translate .1s;
    /* icons carry built-in whitespace; the negative margin cancels it on both sides.
       :only-child ignores text nodes, so icon-only buttons are keyed on aria-label */
    & svg { inline-size: 1.15em; block-size: 1.15em; margin-inline: -.2em; flex: none }
    &[aria-label]:has(> svg) { padding-inline: .7em }
    &:hover { filter: brightness(1.1) }
    &:active { translate: 0 1px }
    &:disabled { filter: none; translate: none }
  }

  /* control sizes. Interiors are em, so one declaration resizes the whole control. */
  :is(button, .button, input, select, textarea).sm { font-size: .875rem }
  :is(button, .button, input, select, textarea).lg { font-size: 1.125rem }

  /* An input is display:block; inline-size:100%, so an adornment has nowhere to sit. The wrapper
     carries the border and the focus ring; the input inside carries neither. */
  .field {
    display: flex;
    align-items: center;
    gap: .5em;
    padding-inline: .75em;
    border: 1px solid var(--l-border);
    border-radius: var(--l-radius);
    corner-shape: var(--l-corner);
    background: var(--l-bg);
    margin-block-end: var(--l-space);
    &:focus-within { border-color: var(--l-primary); box-shadow: var(--l-focus) }
    & > :is(input, select, textarea) {
      border: 0;
      padding-inline: 0;
      background: none;
      margin-block-end: 0;
      &:focus { box-shadow: none }
    }
    & > :is(svg, span, kbd) { flex: none; color: var(--l-muted) }
    & > svg { inline-size: 1.1em; block-size: 1.1em }
  }

  /* squircles where supported; plain rounded corners elsewhere */
  :is(input, select, textarea, button, .button, fieldset, details, pre, article, .card, dialog, [popover], body > header) {
    corner-shape: var(--l-corner);
  }
}

@layer leicht.components {
  /* variants only repoint variables; .error/.success also recolour .tag, .text-primary, links… */
  .secondary { --_bg: var(--l-surface); --_fg: var(--l-fg); --_bd: var(--l-border) }
  .outline { --_bg: #0000; --_fg: var(--l-primary); --_bd: currentColor }
  .ghost { --_bg: #0000; --_fg: var(--l-primary) }
  .error { --l-primary: var(--l-error) }
  .success { --l-primary: var(--l-success) }
  .warning { --l-primary: var(--l-warning) }
  .neutral { --l-primary: var(--l-muted) }

  /* grid: equal columns by default, 12 tracks once any .col-N appears */
  .row {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: var(--l-space);
    margin-block-end: var(--l-space);
    & > * { margin: 0 }
    &:has(> [class*=col-]) { grid-auto-flow: row; grid-template-columns: repeat(12, minmax(0, 1fr)) }
    & > [class*=col-] { grid-column: span var(--span, 12) }
  }
  /* gap decorations: rules drawn in the gutters (Chrome 149+), ignored elsewhere */
  .row.divided { column-rule: 1px solid var(--l-border); row-rule: 1px solid var(--l-border) }

  .col-1 { --span: 1 } .col-2 { --span: 2 } .col-3 { --span: 3 } .col-4 { --span: 4 }
  .col-5 { --span: 5 } .col-6 { --span: 6 } .col-7 { --span: 7 } .col-8 { --span: 8 }
  .col-9 { --span: 9 } .col-10 { --span: 10 } .col-11 { --span: 11 } .col-12 { --span: 12 }

  /* In a narrow container twelve tracks are unreadable, so small spans pair up two-per-row and
     anything half-width or wider goes full width. Change the two --span values to re-tune it.
     This is a container query, so a row inside a sidebar stacks even on a wide screen. */
  @container (width < 40rem) {
    .row > :is([class*=col-], [data-span]) { --span: 12 }
    .row > :is(.col-1, .col-2, .col-3, .col-4, .col-5) { --span: 6 }
    /* a bare .row has no spans to re-point, so the flow itself is the only thing left to change */
    .row:not(:has(> :is([class*=col-], [data-span]))) { grid-auto-flow: row }
  }

  nav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--l-space-sm) var(--l-space);
    /* ol too: a breadcrumb is an ordered list, and it rendered as "1. 2. 3." without this */
    & :is(ul, ol) { display: flex; flex-wrap: wrap; align-items: center; gap: inherit; margin: 0; padding: 0; list-style: none }
    & a:not(.button) {
      color: inherit;
      text-decoration: none;
      &:is(:hover, [aria-current]) { color: var(--l-primary) }
    }
  }

  article, .card {
    --_p: var(--l-space-lg);
    margin-block-end: var(--l-space);
    padding: var(--_p);
    border: 1px solid var(--l-border);
    border-radius: calc(var(--l-radius) * 1.5);
    background: var(--l-surface);
    & > :is(header, footer) { margin-inline: calc(-1 * var(--_p)); padding: var(--l-space-md) var(--_p) }
    & > header { margin-block: calc(-1 * var(--_p)) var(--_p); border-block-end: 1px solid var(--l-border) }
    & > footer { margin-block: var(--_p) calc(-1 * var(--_p)); border-block-start: 1px solid var(--l-border) }
    /* a borderless button has no edge to align, so its label should sit on the content edge
       like the text above it, not 1.1em inside the padding box */
    & > footer > .ghost:first-child { margin-inline-start: -1.1em }
    /* a card title with a trailing action or menu is the most common card there is */
    & > header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--l-space-sm);
      /* a heading's 1.6em top margin is prose rhythm; inside a header it is just a gap */
      & :is(h1, h2, h3, h4, h5, h6) { margin-block: 0 }
    }
  }

  /* .row owns the gutter and the bottom margin; a card inside must not add a second one.
     This sits after `article, .card` deliberately: at equal specificity source order decides,
     and the reset inside .row above is overridden by the card's own margin. */
  .row > * { margin-block: 0 }
  .row > * > :last-child { margin-block-end: 0 }

  /* ---- glass: the page header floats as a toolbar; overlays and .glass share the material ---- */
  body > header {
    inline-size: min(var(--l-width), 100% - 1rem);
    position: sticky;
    inset-block-start: var(--l-space-sm);
    z-index: 1;
    margin-block: var(--l-space-sm);
    padding-block: var(--l-space-sm);
    border-radius: calc(var(--l-radius) * 1.5);
  }
  :is(body > header, dialog, [popover], .glass) {
    border: 0;
    background: var(--l-glass);
    backdrop-filter: var(--l-blur);
    box-shadow: var(--l-glass-edge), var(--l-shadow);
  }
  :is(dialog, [popover]) { background: var(--l-glass-top) }

  /* callout. .error / .success repoint --l-primary, so the variants come for free */
  .alert {
    display: flex;
    align-items: start;
    gap: var(--l-space-md);
    margin-block-end: var(--l-space);
    padding: var(--l-space-md) var(--l-space);
    border: 1px solid color-mix(in oklab, var(--l-primary) 35%, #0000);
    border-radius: var(--l-radius);
    corner-shape: var(--l-corner);
    background: color-mix(in oklab, var(--l-primary) 10%, var(--l-bg));
    & > :last-child { margin-block-end: 0 }
    & svg { flex: none; inline-size: 1.25em; block-size: 1.25em; color: var(--l-primary) }
  }

  /* joined controls: a segmented bar out of ordinary buttons */
  .group {
    display: inline-flex;
    vertical-align: middle;
    & > * {
      border-radius: 0;
      margin-inline-start: -1px;
      &:hover, &:focus-visible { position: relative; z-index: 1 }
    }
    & > :first-child {
      margin-inline-start: 0;
      border-start-start-radius: var(--l-radius);
      border-end-start-radius: var(--l-radius);
    }
    & > :last-child {
      border-start-end-radius: var(--l-radius);
      border-end-end-radius: var(--l-radius);
    }
  }

  /* a scroll-snap list with dots, from plain markup. ::scroll-marker is Chromium-only for now;
     elsewhere it stays an ordinary horizontal scroller. */
  .carousel {
    display: flex;
    gap: var(--l-space);
    margin-inline: 0;
    padding: 0;
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-marker-group: after;
    scrollbar-width: none;
    & > * { flex: 0 0 min(100%, 24rem); scroll-snap-align: center; margin: 0 }
    &::scroll-marker-group { display: flex; justify-content: center; gap: var(--l-space-sm); padding-block-start: var(--l-space-md) }
    & > *::scroll-marker {
      content: "";
      inline-size: .55rem;
      block-size: .55rem;
      border-radius: 50%;
      background: var(--l-border);
      transition: background .2s, scale .2s;
    }
    & > *::scroll-marker:target-current { background: var(--l-primary); scale: 1.25 }
  }

  /* dialog and popover share one enter/exit transition */
  dialog, [popover] {
    padding: var(--l-space-lg);
    border-radius: calc(var(--l-radius) * 2);
    color: var(--l-fg);
    opacity: 0;
    scale: .96;
    transition: .2s allow-discrete;
    transition-property: opacity, scale, display, overlay;
    &:is([open], :popover-open) {
      opacity: 1;
      scale: 1;
      @starting-style { opacity: 0; scale: .96 }
    }
    & footer { display: flex; justify-content: end; gap: var(--l-space-sm) }
  }
  dialog {
    inline-size: min(32rem, 100% - 2rem);
    &::backdrop { background: #0004; backdrop-filter: blur(3px) }
  }

  /* Every popover gets the material, the origin and the radius. Menu geometry is opted into by
     having a <menu> or <ul> child: a popover holding a form is a panel, and forcing its buttons
     into full-width menu rows made its footer unstyleable from outside the layer. */
  [popover] {
    transform-origin: 0 0;
    &:dir(rtl) { transform-origin: 100% 0 }
    border-radius: calc(var(--l-radius) * 1.5);
  }

  /* a panel is centred by the UA; give it room to hold a form */
  [popover]:not([popover=hint], menu, ul, :has(> :is(menu, ul))) { padding: var(--l-space) }

  /* popover menu, anchored to its invoker; unsupported browsers centre it.
     Two shapes count as a menu: <menu popover> itself, which is the idiomatic markup, and a
     popover wrapping one — a header and a list, say. */
  :is(menu, ul)[popover], [popover]:has(> :is(menu, ul)) {
    min-inline-size: 12rem;
    padding: var(--l-space-xs);
    & :is(ul, menu) { margin: 0; padding: 0; list-style: none }
    & :is(a, button) {
      --_bg: #0000;
      --_fg: var(--l-fg);
      display: flex;
      align-items: center;
      gap: .6em;
      inline-size: 100%;
      justify-content: start;
      padding: .4em .6em;
      border-radius: var(--l-radius);
      background: var(--_bg);
      color: var(--_fg);
      box-shadow: none;
      text-decoration: none;
      & svg { inline-size: 1.1em; block-size: 1.1em; opacity: .7 }
      &:hover { --_bg: var(--l-primary); --_fg: var(--l-on-primary); filter: none }
    }
    /* not scoped to :popover-open: the closing frame must stay anchored */
    @supports (position-area: block-end) {
      inset: auto;
      margin: var(--l-space-xs) 0 0;
      position-anchor: auto;
      position-area: block-end span-inline-end;
      position-try-fallbacks: flip-block, flip-inline;
    }
  }

  /* an avatar is an <img> or a pair of initials; both want the same box */
  .avatar {
    display: inline-grid;
    place-items: center;
    flex: none;
    inline-size: var(--size, 2rem);
    block-size: var(--size, 2rem);
    border-radius: 50%;
    corner-shape: var(--l-corner);
    background: var(--l-primary);
    color: var(--l-on-primary);
    font-size: calc(var(--size, 2rem) * .4);
    font-weight: 600;
    object-fit: cover;
    overflow: hidden;
  }

  /* a status dot reads at a glance where a .tag is too heavy; it inherits the variant colour */
  .dot {
    display: inline-block;
    inline-size: .5em;
    block-size: .5em;
    border-radius: 50%;
    background: var(--l-primary);
    margin-inline-end: .45em;
    vertical-align: .05em;
  }

  /* tag: a pill, or a luggage-label shape where border-shape exists */
  .tag {
    display: inline-block;
    padding: .1em .6em;
    border: 1px solid color-mix(in oklab, var(--l-primary) 35%, #0000);
    border-radius: 99em;
    background: color-mix(in oklab, var(--l-primary) 14%, #0000);
    color: var(--l-primary);
    font-size: .8em;
    font-weight: 600;
    @supports (border-shape: polygon(round 1px, 0 0, 1px 0, 0 1px)) {
      padding-inline: .9em .6em;
      border-shape: polygon(round 3px, .55em 0, 100% 0, 100% 100%, .55em 100%, 0 50%);
    }
  }
}

/* The application layer: a shell, a data table, and the chrome an app needs that a document does
   not. Everything here is gated — a page with no <aside>, no <figure><table>, no [role=tablist]
   and no <output> is byte-for-byte unaffected. */

@layer leicht.components {
  /* ---- shell. Built from body's own children, so every body > … rule keeps matching: the glass
     toolbar, the page gutters and the muted footer all still apply. ---- */
  body:has(> aside) {
    --l-width: none;
    display: grid;
    grid-template-columns: var(--l-aside) 1fr;
    grid-template-rows: auto 1fr auto;
  }
  body:has(> aside) > aside { grid-area: 1 / 1 / -1 / 2 }
  body:has(> aside) > :is(header, main, footer) {
    grid-column: 2;
    max-inline-size: none;
    /* body > main sets margin-inline: auto, and auto margins beat justify-self: stretch — the
       content column collapses to its min-content width without this. [measured, Chromium 153] */
    margin-inline: 0;
  }
  body:has(> aside) > header {
    grid-row: 1;
    /* inline-size: min(var(--l-width), …) is invalid once --l-width is none, so reset it here
       rather than relying on the cascade */
    inline-size: auto;
  }
  body:has(> aside) > main { grid-row: 2 }
  body:has(> aside) > footer { grid-row: 3 }

  body > aside {
    padding: var(--l-space);
    border-inline-end: 1px solid var(--l-border);
    background: var(--l-surface);
    & > :first-child { margin-block-start: 0 }
  }

  /* a sidebar nav is vertical, and its rows are targets rather than inline links */
  aside nav {
    display: block;
    & :is(ul, ol) { display: grid; gap: var(--l-space-xs); inline-size: 100% }
    & a:not(.button) {
      display: flex;
      align-items: center;
      gap: .6em;
      padding: .45em .6em;
      border-radius: var(--l-radius);
      corner-shape: var(--l-corner);
      &:hover { background: color-mix(in oklab, var(--l-fg) 6%, #0000); color: inherit }
      &[aria-current] { background: color-mix(in oklab, var(--l-primary) 14%, #0000) }
      & svg { inline-size: 1.1em; block-size: 1.1em; flex: none; opacity: .75 }
    }
  }

  /* ---- <aside popover>: one element, two behaviours. Wide, it is a static column and the
     popover material is unset; narrow, it is an off-canvas drawer the UA opens, light-dismisses
     and transitions for free from a popovertarget button — no script.
     Keep this beside the [popover] rules in components.css: anything added there must be unset
     here, or the desktop column silently inherits it. ---- */
  @media (width >= 60rem) {
    body > aside[popover] {
      display: block;
      position: static;
      inline-size: auto;
      block-size: auto;
      max-inline-size: none;
      max-block-size: none;
      margin: 0;
      padding: var(--l-space);
      border: 0;
      border-inline-end: 1px solid var(--l-border);
      border-radius: 0;
      background: var(--l-surface);
      backdrop-filter: none;
      box-shadow: none;
      opacity: 1;
      scale: 1;
      overlay: none;
    }
  }
  @media (width < 60rem) {
    body:has(> aside) { grid-template-columns: 1fr }
    body:has(> aside) > :is(header, main, footer) { grid-column: 1 }
    body > aside[popover] {
      position: fixed;
      inset: 0 auto 0 0;
      inline-size: 16rem;
      max-inline-size: 80vw;
      /* the UA gives a popover height: fit-content, which defeats the inset pair and leaves the
         drawer floating at the top as a box [measured, Chromium 153] */
      block-size: auto;
      max-block-size: none;
      margin: 0;
      overflow-y: auto;
      border-radius: 0;
      border-inline-end: 1px solid var(--l-border);
      transform-origin: 0 50%;
    }
  }
}

@layer leicht.components {
  /* ---- <figure><table>: semantic HTML's own table wrapper. The figure is the scrollport, so a
     wide table scrolls inside its own frame instead of pushing the page sideways. ---- */
  figure:has(> table) {
    overflow: auto;
    max-block-size: var(--l-table-max);
    border: 1px solid var(--l-border);
    border-radius: var(--l-radius);
    corner-shape: var(--l-corner);
    & > table { margin: 0 }
    & > figcaption {
      position: sticky;
      inset-inline-start: 0;
      padding: var(--l-space-sm) var(--l-space-md);
      border-block-start: 1px solid var(--l-border);
    }
    /* Sticky only pays off when the figure has a height to scroll within: overflow-x: auto
       forces overflow-y from visible to auto, so the figure — not the page — is the scrollport,
       and a thead stuck to a box that never scrolls vertically does nothing. Hence
       --l-table-max. [measured, Chromium 153] */
    & thead th {
      position: sticky;
      inset-block-start: 0;
      background: var(--l-bg);
    }
  }

  tbody tr:hover { background: var(--l-surface) }

  /* CSS cannot infer a column's alignment from its header, so numeric cells are marked. <data>
     is the semantic way to say "this is a value"; .num is the escape hatch for a plain cell. */
  :is(th, td).num, td:has(> data) { text-align: end }
}

@layer leicht.components {
  /* ---- tabs: the ARIA that makes them announce correctly is the hook that styles them ---- */
  [role=tablist] {
    display: flex;
    gap: var(--l-space);
    overflow-x: auto;
    scrollbar-width: none;
    margin-block-end: var(--l-space);
    border-block-end: 1px solid var(--l-border);
  }
  [role=tab] {
    --_bg: #0000;
    --_fg: var(--l-muted);
    flex: none;
    padding: var(--l-space-sm) 0;
    border: 0;
    border-radius: 0;
    border-block-end: 2px solid #0000;
    margin-block-end: -1px;
    &:hover { filter: none; --_fg: var(--l-fg) }
    &[aria-selected=true] { --_fg: var(--l-fg); border-block-end-color: var(--l-primary) }
  }

  /* ---- toasts: <output> is already the live region for "a result the page produced". Scoped to
     a direct child of body, so an <output> inside a form or a label stays inline. ---- */
  body > output {
    position: fixed;
    inset-block-end: var(--l-space);
    inset-inline-end: var(--l-space);
    z-index: 2;                     /* body > header is 1 */
    display: grid;
    gap: var(--l-space-sm);
    max-inline-size: 22rem;
    & > .alert {
      margin: 0;
      background: var(--l-glass-top);
      backdrop-filter: var(--l-blur);
      box-shadow: var(--l-shadow);
    }
  }

  /* ---- [aria-busy]: the attribute a screen reader needs is the attribute that draws it ---- */
  [aria-busy=true]:not(button) {
    display: inline-block;
    border-radius: var(--l-radius);
    color: #0000;
    background: linear-gradient(90deg, var(--l-surface), color-mix(in oklab, var(--l-fg) 9%, var(--l-bg)), var(--l-surface)) 0 0 / 200% 100%;
    animation: l-shimmer 1.4s linear infinite;
  }
  @keyframes l-shimmer { to { background-position: -200% 0 } }

  button[aria-busy=true] {
    pointer-events: none;
    opacity: .7;
    &::before {
      content: "";
      inline-size: 1em;
      block-size: 1em;
      border: 2px solid #0006;
      border-block-start-color: currentColor;
      border-radius: 50%;
      animation: l-spin .7s linear infinite;
    }
  }
  @keyframes l-spin { to { rotate: 1turn } }

  /* ---- pagination: a nav whose list is ordered, because page order is meaningful ---- */
  nav[aria-label] > ol {
    justify-content: center;
    gap: var(--l-space-xs);
    & [aria-current] { --_bg: var(--l-primary); --_fg: var(--l-on-primary); --_bd: #0000 }
  }

  /* ---- a dl is a two-column table of one thing each: totals, metadata, specs ---- */
  dl {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--l-space-xs) var(--l-space);
    font-size: .9375rem;
    & dt { color: var(--l-muted) }
    & dd { margin: 0; text-align: end }
  }
}

/* Bleeding edge. Every rule here is an enhancement: a browser that doesn't
   know it drops the declaration and keeps the look from the files above.
   Never put an unsupported function inside a custom property: the property
   would still be "valid" and poison every var() that reads it. */

@view-transition { navigation: auto }

@layer leicht.components {
  html { scrollbar-color: var(--l-border) #0000 }

  /* arrows, chevrons and other directional icons: mark them and they mirror in RTL */
  :dir(rtl) [data-flip] { scale: -1 1 }

  @supports (color: contrast-color(red)) {
    :root { --l-on-primary: contrast-color(var(--l-primary)) }
  }

  /* optical centring: trim the line box to cap height, then pay it back in padding */
  /* trims leading so headings sit on their margins; flex buttons can't be trimmed */
  @supports (text-box: trim-both cap alphabetic) {
    h1, h2, h3, h4 { text-box: trim-both cap alphabetic }
  }

  /* the toolbar starts flat and turns to glass as the page scrolls under it */
  @supports (animation-timeline: scroll()) {
    body > header {
      animation: l-lift linear both;
      animation-timeline: scroll();
      animation-range: 0 5rem;
    }
    @keyframes l-lift {
      from { background-color: #0000; background-image: none; box-shadow: none; backdrop-filter: blur(0) saturate(1) }
    }
  }

  /* a real specular rim: a gradient painted only in the border area */
  @supports (background-clip: border-area) {
    :root { --l-rim: linear-gradient(light-dark(#fff, #fff8), light-dark(#fff3, #fff1) 40%, light-dark(#fff8, #fff3)) border-area }
    /* two rules: an unknown ::picker() would void a shared selector list */
    :is(body > header, .glass) { border: 1px solid #0000; background: var(--l-rim), var(--l-glass) padding-box }
    :is(dialog, [popover]:not([popover=hint])) { border: 1px solid #0000; background: var(--l-rim), var(--l-glass-top) padding-box }
    ::picker(select) { border: 1px solid #0000; background: var(--l-rim), var(--l-glass-top) padding-box }
  }

  /* toolbar slides away while scrolling down, back on the way up; stays while its menu is open */
  html { container-type: scroll-state }
  body > header { transition: translate .3s cubic-bezier(.2, .7, .3, 1) }
  @container scroll-state(scrolled: bottom) {
    body > header:not(:has(:popover-open, :focus-within)) { translate: 0 calc(-100% - 1rem) }
  }

  /* <div data-span="5"> works as well as .col-5 */
  .row:has(> [data-span]) { grid-auto-flow: row; grid-template-columns: repeat(12, minmax(0, 1fr)) }
  .row > [data-span] { --span: attr(data-span type(<integer>)); grid-column: span var(--span, 12) }

  @supports (min-inline-size: anchor-size(width)) {
    /* menus only: a panel must not be squeezed to its invoker's width */
    :is(menu, ul)[popover], [popover]:has(> :is(menu, ul)) { min-inline-size: max(12rem, anchor-size(width)) }
  }

  /* menu items cascade in */
  :is(:is(menu, ul)[popover], [popover]:has(> :is(menu, ul))):popover-open li {
    transition: opacity .2s, translate .2s;
    transition-delay: calc(sibling-index() * 25ms);
    @starting-style { opacity: 0; translate: 0 -.25rem }
  }

  /* <select> with a real, stylable, glass dropdown. Chromium and WebKit both ship it. */
  @supports (appearance: base-select) {
    select, ::picker(select) { appearance: base-select }
    select { display: flex; align-items: center; justify-content: space-between; gap: .5em }
    select::picker-icon { transition: rotate .2s }
    select:open::picker-icon { rotate: 180deg }
    ::picker(select) {
      position-try-order: normal;
      margin-block: var(--l-space-xs);
      padding: var(--l-space-xs);
      border: 0;
      border-radius: calc(var(--l-radius) * 1.5);
      corner-shape: var(--l-corner);
      background: var(--l-glass-top);
      backdrop-filter: var(--l-blur);
      box-shadow: var(--l-glass-edge), var(--l-shadow);
      /* the open state must be the BASE state, faded in from @starting-style. Hiding the picker
         with opacity: 0 and revealing it on :popover-open leaves WebKit's picker open but
         invisible, which reads as "the select does not work". [measured, WebKit 26.5] */
      transition: opacity .2s, display .2s allow-discrete, overlay .2s allow-discrete;
      @starting-style { opacity: 0 }
    }
    option {
      padding: .4em .6em;
      border-radius: var(--l-radius);
      &:is(:hover, :focus-visible) { background: var(--l-primary); color: var(--l-on-primary); outline: 0 }
      &::checkmark { order: 1; margin-inline-start: auto }
    }
  }

  /* Apple-style liquid glass, opt-in with .liquid. Three stacked layers, as the material works:
     a refraction layer that distorts the captured backdrop, a tint, and a specular rim.
     The distortion is `filter` on that layer rather than url() inside backdrop-filter — the same
     trick the reference pen uses, and it may reach WebKit, which refuses url() in backdrop-filter.
     Both pseudo-elements sit at z-index -1: painted above the element's background, below its
     content, so no extra markup is needed. */
  .liquid {
    position: relative;
    isolation: isolate;
    border: 0;
    background: none;
    box-shadow: var(--l-shadow);
    &::before, &::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      border-radius: inherit;
      corner-shape: var(--l-corner);
    }
    &::before {
      backdrop-filter: blur(var(--l-liquid-blur)) saturate(var(--l-liquid-sat));
      filter: var(--l-refract);
    }
    &::after {
      background: color-mix(in oklab, var(--l-bg) var(--l-liquid-tint), #0000);
      box-shadow: inset 1px 1px 0 var(--l-liquid-edge), inset 0 0 5px var(--l-liquid-edge);
    }
  }
  @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
    .liquid::before { backdrop-filter: none; filter: none }
  }

  /* <button interestfor="tip"> + <div id="tip" popover="hint">.
     Edge-aligned, not centred: a centred tooltip gets shoved off its anchor near the viewport
     edge and its tail stops pointing at anything. The tail follows each flip via anchored queries. */
  [popover=hint] {
    --_t: .35rem;
    min-inline-size: 0;
    inline-size: max-content;
    max-inline-size: 20rem;
    padding: .4rem .7rem;
    border: 0;
    border-radius: var(--l-radius);
    background: var(--l-fg);
    color: var(--l-bg);
    backdrop-filter: none;
    box-shadow: none;
    font-size: .9375rem;
    overflow: visible;
    transform-origin: 1rem 100%;
    &:dir(rtl) { transform-origin: calc(100% - 1rem) 100% }
    @supports (position-area: block-start) {
      container-type: anchored;
      margin: 0 0 calc(var(--_t) + .2rem);
      position-area: block-start span-inline-end;
      position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline;
      &::before {
        content: "";
        position: absolute;
        inset-block-start: calc(100% - 1px);
        inset-inline-start: calc(1.1rem - var(--_t));
        inline-size: calc(var(--_t) * 2);
        block-size: calc(var(--_t) + 1px);
        background: inherit;
        clip-path: polygon(0 0, 100% 0, 50% 100%);
      }
    }
  }
  @container anchored(fallback: flip-inline) or anchored(fallback: flip-block flip-inline) {
    [popover=hint]::before { inset-inline: auto calc(1.1rem - var(--_t)) }
  }
  @container anchored(fallback: flip-block) or anchored(fallback: flip-block flip-inline) {
    [popover=hint]::before { inset-block: auto calc(100% - 1px); clip-path: polygon(50% 0, 100% 100%, 0 100%) }
  }
}

@layer leicht.utilities {
  .text-center { text-align: center }
  .text-muted { color: var(--l-muted) }
  .text-primary { color: var(--l-primary) }
  .flex { display: flex; flex-wrap: wrap; flex-wrap: balance; align-items: center; gap: var(--gap, var(--l-space)) }
  .stack { display: grid; gap: var(--gap, var(--l-space)) }
  .full-width { inline-size: 100% }
  .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-inline-size: 0 }
  /* A control is display:block; inline-size:100%, which makes each one claim a whole row inside
     a toolbar. In a .flex row they should size to their content, the way .field's children do. */
  .flex > :is(input, select, textarea) { inline-size: auto }
  .between { justify-content: space-between }
  .end { justify-content: flex-end }
  .sr-only { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap }
  @media (width < 40rem) { .hide-sm { display: none } }
  @media (width >= 40rem) { .hide-lg { display: none } }

  @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth } }
  /* ::before/::after are not matched by *, so a pseudo-element animation — the button spinner,
     for one — kept running under reduced motion until they were named here */
  @media (prefers-reduced-motion: reduce) {
    *, ::before, ::after, ::backdrop { transition: none; animation: none }
  }
  @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) { :root { --l-glass: var(--l-bg); --l-blur: none } }
}

/* Quiet polish: things nobody notices until they are missing.
   In the utilities layer so the accessibility overrides beat every component rule. */
@layer leicht.utilities {
  ::selection { background: color-mix(in oklab, var(--l-primary) 30%, #0000) }

  /* deep links land below the sticky toolbar */
  :target { scroll-margin-block-start: 5rem }

  /* a skip link with no class: the first link in <body>, visible only on focus */
  body > a[href^="#"]:first-child:not(:focus) {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  body > a[href^="#"]:first-child:focus {
    position: fixed;
    inset: .75rem auto auto .75rem;
    z-index: 99;
    padding: .5em 1em;
    border-radius: var(--l-radius);
    background: var(--l-primary);
    color: var(--l-on-primary);
    text-decoration: none;
  }

  /* a hint next to a field turns into an error once the user has actually touched it */
  :is(input, select, textarea):user-invalid + small {
    color: var(--l-error);
    &::before { content: "⚠ " }
  }

  /* photos glare in dark mode; SVG and icons are left alone */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme=light]) :is(img, video):not([src$=".svg"]) { filter: brightness(.92) contrast(1.02) }
  }
  :root[data-theme=dark] :is(img, video):not([src$=".svg"]) { filter: brightness(.92) contrast(1.02) }

  /* an empty list or table body says so, instead of collapsing to nothing */
  :is(tbody, ul, ol, .stack)[data-empty]:empty::after {
    content: attr(data-empty);
    color: var(--l-muted);
    font-style: italic;
  }

  /* Windows High Contrast: drop the glass, keep real borders */
  @media (forced-colors: active) {
    .glass, .liquid, body > header, dialog, [popover], body > output > .alert {
      backdrop-filter: none;
      background: Canvas;
      border: 1px solid CanvasText;
    }
    button, [type=submit], [type=button], [type=reset], .button { border: 1px solid ButtonText }
  }

  @media print {
    :root { --l-bg: #fff; --l-fg: #000; --l-glass: #fff; --l-blur: none; color-scheme: light }
    body > :is(header, footer, output), nav, dialog, [popover], .button, button { display: none }
    body > main, .container { max-inline-size: none; padding: 0 }
    :is(article, .card, pre, blockquote, table, figure, details) {
      break-inside: avoid;
      border: 1px solid #999;
      background: none;
      box-shadow: none;
    }
    :is(h1, h2, h3, h4) { break-after: avoid }
    details { display: block }
    details::details-content { block-size: auto; content-visibility: visible }
    /* a printed link is useless without its destination */
    main a[href^="http"]::after { content: " (" attr(href) ")"; font-size: .8em; word-break: break-all }
    @page { margin: 18mm }
  }
}

