/* ── icon: a mark on the same axes as the type beside it ──────────────────────
 *
 * Companion to Icon.tsx. The host loads Material Symbols Outlined with all four axes;
 * this file decides what the mark does with them.
 *
 * THE THREE INKS ARE THE THREE STATES. --ink-faint / --ink-quiet / --ink-full already
 * carry "cannot act", "here but not chosen" and "chosen" everywhere else in the system,
 * so an icon uses those and nothing new. The novelty is that the WEIGHT moves with the
 * ink: hover is not a colour change with a shape underneath it, it is the mark pressing
 * harder. That is the whole reason to leave SVG behind, so it is stated here first.
 *
 *   off     ink-faint  wght 200   the control cannot act -- a frozen axis, a disabled row
 *   rest    ink-faint  wght 200   present, not chosen
 *   hover   ink-quiet  wght 400   under the pointer
 *   active  ink-full   wght 600   chosen. FILL 1 if the mark has a filled form.
 *
 * THREE INKS, THREE STATES, and no two of them share one. Hover and active were both
 * ink-full first, so the colour said nothing -- the mark reached its final ink merely by
 * being pointed at, and arriving at `active` changed only the weight. A weight step is
 * not enough on its own to carry "this is the one that is on": the ladder has to climb
 * faint -> quiet -> full so that hover is visibly a WAY to active rather than the same
 * thing as it.
 *
 * 200 -> 400 rather than 400 -> 500: at 20px a 100-unit step is not visible, and the
 * point of putting emphasis on an axis is that you can SEE the mark press. Doubling the
 * weight is legible without the mark changing size, which is the thing a swap of two
 * drawings could never do. It also leaves the resting rail quieter than the type it
 * labels, which is where a row of marks belongs.
 *
 * GRAD, NOT wght, CARRIES THE THEME. On a dark ground a stroke of the same weight reads
 * heavier -- it blooms. GRAD exists for exactly this and moves the stroke without moving
 * the glyph's metrics, so nothing reflows when the theme flips. wght is left free to mean
 * emphasis and only emphasis; if the theme used wght too, a hovered icon in dark mode and
 * a resting icon in light mode would be the same drawing.
 *
 * NO BOX, and no hit area of its own. The mark is a mark. Where something must be
 * pressable, the padding belongs to the button that wraps it -- .wm-icon-btn below is
 * that button, and it draws nothing. This is the same rule the dial learned the hard way:
 * a target has to exist somewhere, but it does not have to be visible, and it must not be
 * bought by shrinking the thing it wraps.
 *
 * @layer wm.icon so an app's plain rule always wins. See DIAL.md §6.
 * ─────────────────────────────────────────────────────────────────────────── */

/* SELF-HOSTED, and outside the layer because @font-face is not a style rule and cascade
   layers do not apply to it.
   The package ships the font in fonts/ beside Cal Sans, so a consumer gets the marks by
   importing this file and nothing else -- no <link> to remember, which is exactly the
   thing that would have made ReCal render `format_align_left` as words.
   IT IS A SUBSET: 76 ligatures, 56KB, not the 3.9MB face. This comment said the opposite
   until 2026-09-20 -- that the full face shipped so a drifting subset could never fail --
   and the artifact had been a subset all along. The failure a subset has is also not the
   one that comment feared: a name outside it does not print as words, the ligature simply
   never forms and the browser draws whatever component glyphs the font happens to hold,
   so `undo` came out as two strokes and `arrow_outward` as a circle, and both passed as
   icons in a screenshot. So the guard is a lint, not a bigger file: scripts/lint-icons.py
   reads the GSUB and fails any name in the code the face cannot draw, here and in every
   consumer CI builds. Growing the subset: pyftsubset from the full face,
   --layout-features=liga, the union of every name in use; then both copies (fonts/ and
   docs/fonts/, which build.py syncs).
   ?v= is the ligature count: the face is re-cut under the same name whenever an icon joins it,
   and a cached copy would draw the new name as its letters. cut-icon-subset.py rewrites it,
   lint-icons.py holds it.
   The path is relative to THIS stylesheet, so it resolves the same whether the file is
   read from src/ in the package or from shared/src/ in a consumer. */
@font-face {
  font-family: 'Material Symbols Outlined';
  font-style: normal;
  font-weight: 100 700;
  font-display: block;   /* block, not swap: a fallback would print the ligature NAME */
  src: url('../fonts/MaterialSymbolsOutlined.woff2?v=89') format('woff2');
}

/* --icon-hdr has to be REGISTERED or it cannot be transitioned: an unregistered custom
   property is a token stream, and a token stream has no interpolation. That registration
   is the whole reason the boost can be a slope rather than a switch. */
@property --icon-hdr {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

@layer wm.icon {
  .wm-icon {
    /* The literal ligature name is the content, so it must not wrap, hyphenate, or get
       letter-spaced by an inherited caps rule -- any of which breaks the ligature and
       prints the word. */
    font-family: 'Material Symbols Outlined';
    font-weight: normal;
    font-style: normal;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    white-space: nowrap;
    word-wrap: normal;
    direction: ltr;
    -webkit-font-feature-settings: 'liga';
    font-feature-settings: 'liga';
    -webkit-font-smoothing: antialiased;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    /* The mark sits on the text it labels, not on the box. */
    vertical-align: middle;
    user-select: none;

    /* No box. Stated, not merely omitted, because these usually live inside a button
       that used to draw one. */
    background: none;
    border: 0;
    border-radius: 0;
    padding: 0;

    --icon-opsz: 20;
    --icon-fill: 0;
    /* ONE WEIGHT FOR THE WHOLE SET. Emphasis rides GRAD instead, which thickens the
       stroke without touching the advance or the outline -- so a row of marks cannot
       shift by a fraction as one of them lights up, and the marks stay one family whose
       pressure changes rather than four weights of the same drawing. */
    --icon-wght: 300;
    /* Dark ground, at rest. Light starts higher: the same stroke reads lighter there. */
    --icon-grad: 50;

    font-variation-settings:
      'FILL' var(--icon-fill),
      'wght' var(--icon-wght),
      'GRAD' var(--icon-grad),
      'opsz' var(--icon-opsz);

    color: rgba(var(--text-rgb, 232, 232, 232), var(--ink-faint, .38));
    /* --icon-dur, not --dur-fast directly: a hover wants to be instant, but a CONFIRM
       wants to be seen. At 140ms the copy button's fill read as a flicker -- indeed it
       read as the old SVG flashing -- so the affordance that says "it worked" needs to
       be slower than the one that says "you are pointing at this". The caller turns it
       up; nothing else changes. */
    /* 90ms, not --dur-fast's 140: at 140 the mark visibly lagged the row it sits in,
       arriving after the thing it labels had already moved. A rollover should feel like
       the pointer, not like an animation about the pointer. The copy confirm turns this
       up to 260 because that one IS the animation. */
    --icon-dur: 90ms;   /* token-lint: allow -- deliberately shorter than --dur-fast */
    transition:
      font-variation-settings var(--icon-dur) ease,
      color var(--icon-dur) ease,
      --icon-hdr var(--icon-dur) ease;
  }

  /* Light ground: every rung one step up, because the same stroke reads lighter on light.
     rest 100 - hover 150 - active 100. Dark: 50 - 100 - 75. Hover is the top of the
     stroke ladder in both; active comes back DOWN by 50 because its HDR paint carries the
     emphasis instead. Same shape, different absolute numbers -- which is why active is
     restated per theme rather than inherited. */
  @media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .wm-icon { --icon-grad: 100; }
    :root:not([data-theme="dark"]) .wm-icon--off { --icon-grad: 100; }
    :root:not([data-theme="dark"]) .wm-icon--active,
    :root:not([data-theme="dark"]) .active > .wm-icon,
    :root:not([data-theme="dark"]) .active > * > .wm-icon { --icon-grad: 100; }
  }
  :root[data-theme="light"] .wm-icon { --icon-grad: 100; }
  :root[data-theme="light"] .wm-icon--off { --icon-grad: 100; }
  :root[data-theme="light"] .wm-icon--active,
  :root[data-theme="light"] .active > .wm-icon,
  :root[data-theme="light"] .active > * > .wm-icon { --icon-grad: 100; }
  :root[data-theme="dark"] .wm-icon { --icon-grad: 50; }
  :root[data-theme="dark"] .wm-icon--off { --icon-grad: 50; }
  /* Stated for dark as well as light, and this is why: `:root[data-theme="dark"] .wm-icon`
     scores (0,2,1) while `.active > .wm-icon` scores (0,2,0), so the theme's REST value
     beat active outright and the chosen mark never lit. The light block had these rules
     already; dark did not, which is exactly the sort of asymmetry that renders correctly
     in the theme you happen to be testing. */
  /* 75, matching the base active rule: the HDR paint carries the emphasis, so the stroke
     comes back DOWN from the rollover rather than up -- one rung, not off the end. Stated
     per theme for the same
     specificity reason as the rest -- `:root[data-theme] .wm-icon` is (0,2,1) and beats
     `.active > .wm-icon` at (0,2,0), so a theme's value silently wins unless active is
     restated inside it. That is exactly how this rule ended up overriding the 50 it was
     supposed to agree with. */
  :root[data-theme="dark"] .wm-icon--active,
  :root[data-theme="dark"] .active > .wm-icon,
  :root[data-theme="dark"] .active > * > .wm-icon { --icon-grad: 75; }

  .wm-icon--off {
    --icon-wght: 300;
    color: rgba(var(--text-rgb, 232, 232, 232), var(--ink-faint, .38));
  }
  /* ACTIVE COMES FROM THE BUTTON TOO, not only from the prop.
     Icon takes state="active", and Fitting's alignments pass it -- but every other
     control in these apps already says "chosen" by putting .active on the BUTTON, and
     was doing so long before there was an Icon. Reading only the prop meant an active
     mode row and a resting one drew the same mark: the row lit up, the mark did not, so
     the emphasis vanished at exactly the moment it was supposed to arrive.
     Matching the host's own class here rather than editing seventeen call sites, and
     because the button is the thing that actually knows. */
  /* ACTIVE IS THE ONLY STATE THAT LEAVES SDR. The ladder spends its ink and its GRAD
     getting here -- faint to quiet to full, 50 to 100 to 150 -- and then has nowhere left
     to go on a screen that can go further. So the chosen mark is painted with a flat PQ
     white rather than with ink: the same technique the poster uses on its headline, and
     literally the same light -- this swatch is one pixel of field-247, its brightest,
     which works out at 1216 nits through the PQ EOTF. dynamic-range-limit:no-limit is
     what allows past SDR white.

     NOT PQ 1.0, deliberately. That is 10,000 nits, which no display resolves, so the
     system tone-maps it -- and clipping is where a bright white turns chalky and shifts
     hue. Sampling the field's own peak also means an active mark glows at exactly the
     wordmark's white rather than outshining the logotype from 20px.

     TWO LAYERS, so the boost is a SLOPE. The bottom is the HDR white; the top is the
     ordinary ink at alpha (1 - --icon-hdr). At 0 the ink is opaque and the mark looks
     exactly as it did, which is why the handover is invisible; as --icon-hdr climbs the
     ink thins and the HDR comes through. background-image and background-clip cannot be
     interpolated, so nothing about the paint animates -- only that one number, registered
     above so that it can.

     On an SDR display the bottom layer is just white and the mark reads as full ink,
     which is where the ladder ended anyway. Nothing is lost by not having the screen. */
  .wm-icon--active,
  .active > .wm-icon,
  .active > * > .wm-icon {
    /* GRAD comes DOWN from the rollover, by 50. Everywhere else on this ladder emphasis is
       stroke weight, because that is all an SDR mark has; once the mark is painted above
       SDR white the luminance does that work, and a heavier stroke on top is two emphases
       stacked -- a blob at 20px.
       The step down is RELATIVE and each theme states its own END value: dark 75, light
       100 -- a rung BELOW that theme's hover, not a plunge. Earlier ends of -50 and then
       -25 were read off the axis rather than off the ladder: the shipped fvar runs GRAD
       -50..200, so -50 is the floor with nothing under it, and the mark went visibly thin
       the moment the pointer left. 75 sits 25 under dark's hover and still reads chosen.
       Worth knowing when tuning it: active is heavier than its GRAD implies because of the
       PAINT PATH, not the axis. `color: transparent` + `background-clip: text` makes
       Chrome rasterise the glyph as a MASK rather than as smoothed coloured text, and a
       mask is full grayscale coverage -- more actual pixels, not bloom.
       -webkit-font-smoothing: antialiased is set on both and does not reach it. */
    --icon-grad: 75;
    --icon-hdr: 1;
    color: transparent;
    background-image:
      linear-gradient(
        rgba(var(--text-rgb, 232, 232, 232), calc(1 - var(--icon-hdr))),
        rgba(var(--text-rgb, 232, 232, 232), calc(1 - var(--icon-hdr)))),
      url('data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAE1pUHJhdmlmbWlhZm1pZjEAAAFebWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAAOcGl0bQAAAAAAAQAAADhpaW5mAAAAAAACAAAAFWluZmUCAAAAAAEAAGF2MDEAAAAAFWluZmUCAAABAAIAAEV4aWYAAAAAGmlyZWYAAAAAAAAADmNkc2MAAgABAAEAAACBaXBycAAAAGBpcGNvAAAAE2NvbHJuY2x4AAkAEAABgAAAAAxjbGxpA+gAQAAAABRpc3BlAAAAAAAAAGQAAABkAAAACWlyb3QAAAAAEHBpeGkAAAAAAwoKCgAAAAxhdjFDgQBMAAAAABlpcG1hAAAAAAAAAAEAAQaBAgMFhoQAAAAsaWxvYwAAAABEAAACAAEAAAABAAAB0AAAADcAAgAAAAEAAAGOAAAAQgAAAAFtZGF0AAAAAAAAAIkAAAAGRXhpZgAATU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAZKADAAQAAAABAAAAZAAAAAASAAoNAAAAAzY8Z//ChIgAwjIkEAGEgAYYYYi0VMregO56Wg+x6xnwsfoJWaYWX85G+ibLVJ6A');
    background-size: 100% 100%;
    background-repeat: no-repeat;
    -webkit-background-clip: text;
    background-clip: text;
    dynamic-range-limit: no-limit;
  }

  /* LIGHT GROUND TAKES NO HDR, and this is physics rather than taste. The boost exists
     only for pixels brighter than SDR white; a mark on a light ground has to go DARKER to
     read as chosen, and there is no such thing as darker-than-black to boost into. Left
     ungated, the swatch painted the chosen mark WHITE on a light page -- the faintest
     thing in the row rather than the fullest, which is the exact inverse of what active
     means. Everything is unwound explicitly: background-clip back to border-box so the
     glyph is smoothed coloured text again and not a mask, and --icon-hdr to 0 so anything
     else keyed to it stands down.
     Stated twice for the same reason the GRAD blocks are -- the media query cannot cover a
     user who has chosen light against an OS set to dark. */
  @media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .wm-icon--active,
    :root:not([data-theme="dark"]) .active > .wm-icon,
    :root:not([data-theme="dark"]) .active > * > .wm-icon {
      --icon-hdr: 0;
      color: rgba(var(--text-rgb, 232, 232, 232), var(--ink-full, 1));
      background-image: none;
      -webkit-background-clip: border-box;
      background-clip: border-box;
      dynamic-range-limit: standard;
    }
  }
  :root[data-theme="light"] .wm-icon--active,
  :root[data-theme="light"] .active > .wm-icon,
  :root[data-theme="light"] .active > * > .wm-icon {
    --icon-hdr: 0;
    color: rgba(var(--text-rgb, 232, 232, 232), var(--ink-full, 1));
    background-image: none;
    -webkit-background-clip: border-box;
    background-clip: border-box;
    dynamic-range-limit: standard;
  }

  /* ACTIVE DOES NOT TAKE HOVER. Every selector below excludes it, on the mark and on the
     button carrying the host's own .active. Without that, pointing at the chosen item
     dropped it from full ink to QUIET and pushed GRAD back up -- an active control getting
     dimmer and heavier under the pointer, which is the opposite of what hover means. It
     also fought the HDR paint, since the veil is keyed to the active state and the ink
     under it had just changed.

     Hover is the pointer's, so it is scoped to the button that owns the pointer rather
     than to the mark: hovering a 20px glyph is a smaller and less forgiving target than
     hovering the control it stands for, and the two should not disagree about state.
     :focus-visible gets the same treatment so the keyboard sees what the mouse sees.
     An `off` mark does not react -- nothing to react to.

     The bare `.wm-icon:hover` at the end of the list needs :not(:where(.active) *) as
     well, and that omission is what put SDR white back over the HDR mask. Active arrives
     two ways -- the `--active` MODIFIER, and a host ancestor carrying `.active` with a
     plain mark inside it (the sidebar's .mode-btn does exactly this). Excluding only the
     modifier leaves the second kind unguarded, so pointing straight at the glyph repainted
     it at ink-quiet. background-clip: text only reveals the image where the text is
     TRANSPARENT, so any colour at all covers the swatch and the HDR vanishes under the
     pointer -- not dimmed, replaced. */
  .wm-icon-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .wm-icon-btn:not(.active):focus-visible .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  /* Buttons that predate .wm-icon-btn and own their own target. Named rather than
     matched on `button:hover .wm-icon`, which would also fire for a button that merely
     contains a mark somewhere -- a toolbar, a row -- and light up a mark the pointer is
     nowhere near. */
  .fit-align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .fit-align-btn:not(.active):focus-visible .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .align-btn:not(.active):focus-visible .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .mobile-tab:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .mobile-sub-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .mode-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  .wm-icon:not(.wm-icon--off):not(.wm-icon--active):not(:where(.active) *):hover {
    /* THE MIDDLE RUNG. Hover was at 200 -- the top of GRAD's range and the same value
       `active` uses -- so in light mode a hovered mark was as heavy as the chosen one,
       and hovering an unchosen mark made it look chosen. Hover has to sit BETWEEN rest
       and active or it is not a way to active, it is a claim to be it. */
    --icon-grad: 100;
    color: rgba(var(--text-rgb, 232, 232, 232), var(--ink-quiet, .62));
  }
  /* The same middle rung on light, where every value sits one step higher. Without this
     a light hover used the dark number and came out lighter than rest. */
  @media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) .wm-icon-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
    :root:not([data-theme="dark"]) .fit-align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
    :root:not([data-theme="dark"]) .align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
    :root:not([data-theme="dark"]) .mobile-tab:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
    :root:not([data-theme="dark"]) .mode-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
    :root:not([data-theme="dark"]) .wm-icon:not(.wm-icon--off):not(.wm-icon--active):not(:where(.active) *):hover { --icon-grad: 150; }
  }
  :root[data-theme="light"] .wm-icon-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="light"] .fit-align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="light"] .align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="light"] .mobile-tab:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="light"] .mode-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="light"] .wm-icon:not(.wm-icon--off):not(.wm-icon--active):not(:where(.active) *):hover { --icon-grad: 150; }
  :root[data-theme="dark"] .wm-icon-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="dark"] .fit-align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="dark"] .align-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="dark"] .mobile-tab:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="dark"] .mode-btn:not(.active):hover .wm-icon:not(.wm-icon--off):not(.wm-icon--active),
  :root[data-theme="dark"] .wm-icon:not(.wm-icon--off):not(.wm-icon--active):not(:where(.active) *):hover { --icon-grad: 100; }

  /* THE BUTTON THAT DRAWS NOTHING. All target, no face: the padding is the hit area, and
     44px is the touch floor rather than a look. Everything visible is the mark. */
  .wm-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* A knob, not a fixed value, because this file lives in @layer wm.icon -- which is
       declared AFTER wm.controls, so a control's stylesheet cannot override it however
       specific it gets: a later layer wins outright. A host or another primitive that
       needs a bigger target sets --icon-btn-pad rather than fighting the cascade and
       silently losing, which is exactly what BlockStyleRail did first. */
    padding: var(--icon-btn-pad, var(--spacing-02, 6px));
    margin: 0;
    background: none;
    border: 0;
    border-radius: 0;
    color: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .wm-icon-btn:focus { outline: none; }
  /* The ring is on the BUTTON, not the mark: an outline on an inline glyph traces the
     text box, which is tight to the ink and reads as a mistake. */
  .wm-icon-btn:focus-visible {
    outline: 2px solid var(--accent, currentColor);
    outline-offset: 2px;
  }

  /* Touch: the mark holds its size, the target grows. Same contract as the dial. */
  @media (pointer: coarse) {
    .wm-icon-btn { min-width: 44px; min-height: 44px; }
  }


  @media (prefers-reduced-motion: reduce) {
    .wm-icon { transition: none; }
  }
}
