/* nilam — the whole system in one import. GENERATED by build.mjs; do not edit.
 *
 * Concatenated from: nilam.tokens.css (solved) + nilam.motion.css + nilam.scale.css + nilam.base.css + nilam.components.css.
 * Import the parts individually if you want the tokens without the components.
 */

/* nilam — GENERATED. Do not hand-edit; change the hue and re-solve.
 *
 *   npx nilam 285 --css=nilam.tokens.css
 *
 * brand hue 285 (नीलम, sapphire) · semantics {"danger":22,"warn":68,"ok":142}
 *
 * Every lightness here was found by inverting a contrast requirement, not chosen. The
 * semantic hues were chosen to maximise the smallest pairwise separation across normal
 * vision, protanopia, deuteranopia and tritanopia.
 * 1096 assertions hold over these exact
 * values; edit one by hand and none of them do.
 *
 * Role model (Radix's — it is good design and there was no reason to reinvent it):
 *   1  page              7  border normal   (WCAG 1.4.11, 3:1)
 *   2  subtle page       8  border hover
 *   3  component rest    9  solid           (the brand moment)
 *   4  component hover  10  solid hover
 *   5  component active 11  text low        (WCAG 1.4.3, 4.5:1)
 *   6  border subtle    12  text high       (7:1)
 *
 * STEP 9 DIFFERS BY MODE, deliberately. A filled button inverts the polarity of the
 * page it sits on: a dark object with light text on a light page, a light object with
 * dark text on a dark one. So always pair it with --<family>-ink and never a literal
 * white. In dark mode step 9 is the GLOW — L 0.66, the lightness Zima Blue and the
 * Telepathy accent both happen to share.
 *
 * THEMING. color-scheme drives everything, so:
 *   <html>                  follows the OS
 *   <html class="dark">     forced dark
 *   <html class="light">    forced light
 *   <div class="dark">      just that subtree, tokens and all
 */

@layer nilam.motion, nilam.tokens, nilam.base, nilam.components, nilam.utilities;

@layer nilam.tokens {
  :root {
    /* Both keywords, so an unstyled page follows the OS. The classes below narrow it. */
    color-scheme: light dark;

    /* neutral */
    --neutral-1: light-dark(oklch(0.9800 0.0070 285.0), oklch(0.1750 0.0070 285.0));
    --neutral-2: light-dark(oklch(0.9617 0.0070 285.0), oklch(0.2120 0.0070 285.0));
    --neutral-3: light-dark(oklch(0.9387 0.0070 285.0), oklch(0.2455 0.0070 285.0));
    --neutral-4: light-dark(oklch(0.9190 0.0070 285.0), oklch(0.2730 0.0070 285.0));
    --neutral-5: light-dark(oklch(0.8981 0.0070 285.0), oklch(0.2979 0.0070 285.0));
    --neutral-6: light-dark(oklch(0.8385 0.0070 285.0), oklch(0.3298 0.0070 285.0));
    --neutral-7: light-dark(oklch(0.6195 0.0070 285.0), oklch(0.5286 0.0070 285.0));
    --neutral-8: light-dark(oklch(0.5417 0.0070 285.0), oklch(0.6057 0.0070 285.0));
    --neutral-9: light-dark(oklch(0.5650 0.0070 285.0), oklch(0.6600 0.0070 285.0));
    --neutral-10: light-dark(oklch(0.5256 0.0070 285.0), oklch(0.7035 0.0070 285.0));
    --neutral-11: light-dark(oklch(0.5228 0.0070 285.0), oklch(0.6257 0.0070 285.0));
    --neutral-12: light-dark(oklch(0.4191 0.0070 285.0), oklch(0.7430 0.0070 285.0));
    --neutral-ink: light-dark(oklch(1.0000 0.0000 285.0), oklch(0.1600 0.0000 285.0));
    --surface: light-dark(oklch(1.0000 0.0000 285.0), oklch(0.2455 0.0000 285.0));
    --ink-max: light-dark(oklch(0.1600 0.0000 285.0), oklch(1.0000 0.0000 285.0));
    --void: light-dark(oklch(0.9000 0.0070 285.0), oklch(0.1000 0.0070 285.0));

    /* brand */
    --brand-1: light-dark(oklch(0.9800 0.0010 285.0), oklch(0.1750 0.0103 285.0));
    --brand-2: light-dark(oklch(0.9618 0.0030 285.0), oklch(0.2126 0.0200 285.0));
    --brand-3: light-dark(oklch(0.9390 0.0079 285.0), oklch(0.2474 0.0377 285.0));
    --brand-4: light-dark(oklch(0.9195 0.0137 285.0), oklch(0.2764 0.0550 285.0));
    --brand-5: light-dark(oklch(0.8991 0.0214 285.0), oklch(0.3031 0.0745 285.0));
    --brand-6: light-dark(oklch(0.8403 0.0379 285.0), oklch(0.3365 0.0906 285.0));
    --brand-7: light-dark(oklch(0.6271 0.1155 285.0), oklch(0.5399 0.1474 285.0));
    --brand-8: light-dark(oklch(0.5558 0.1698 285.0), oklch(0.6159 0.1434 285.0));
    --brand-9: light-dark(oklch(0.5850 0.2186 285.0), oklch(0.6600 0.1739 285.0));
    --brand-10: light-dark(oklch(0.5502 0.2402 285.0), oklch(0.7002 0.1508 285.0));
    --brand-11: light-dark(oklch(0.5369 0.1675 285.0), oklch(0.6341 0.1274 285.0));
    --brand-12: light-dark(oklch(0.4263 0.0997 285.0), oklch(0.7455 0.0547 285.0));
    --brand-ink: light-dark(oklch(1.0000 0.0000 285.0), oklch(0.1600 0.0000 285.0));

    /* danger */
    --danger-1: light-dark(oklch(0.9800 0.0010 22.0), oklch(0.1750 0.0072 22.0));
    --danger-2: light-dark(oklch(0.9620 0.0030 22.0), oklch(0.2129 0.0138 22.0));
    --danger-3: light-dark(oklch(0.9395 0.0079 22.0), oklch(0.2479 0.0261 22.0));
    --danger-4: light-dark(oklch(0.9204 0.0138 22.0), oklch(0.2768 0.0381 22.0));
    --danger-5: light-dark(oklch(0.9005 0.0217 22.0), oklch(0.3034 0.0515 22.0));
    --danger-6: light-dark(oklch(0.8429 0.0396 22.0), oklch(0.3366 0.0625 22.0));
    --danger-7: light-dark(oklch(0.6357 0.1377 22.0), oklch(0.5424 0.1203 22.0));
    --danger-8: light-dark(oklch(0.5594 0.1488 22.0), oklch(0.6253 0.1663 22.0));
    --danger-9: light-dark(oklch(0.5925 0.2197 22.0), oklch(0.6600 0.2088 22.0));
    --danger-10: light-dark(oklch(0.5512 0.2044 22.0), oklch(0.6984 0.1771 22.0));
    --danger-11: light-dark(oklch(0.5388 0.1346 22.0), oklch(0.6432 0.1506 22.0));
    --danger-12: light-dark(oklch(0.4270 0.0689 22.0), oklch(0.7494 0.0605 22.0));
    --danger-ink: light-dark(oklch(1.0000 0.0000 22.0), oklch(0.1600 0.0000 22.0));

    /* warn */
    --warn-1: light-dark(oklch(0.9800 0.0014 68.0), oklch(0.1750 0.0039 68.0));
    --warn-2: light-dark(oklch(0.9618 0.0043 68.0), oklch(0.2121 0.0076 68.0));
    --warn-3: light-dark(oklch(0.9391 0.0113 68.0), oklch(0.2459 0.0142 68.0));
    --warn-4: light-dark(oklch(0.9196 0.0197 68.0), oklch(0.2736 0.0206 68.0));
    --warn-5: light-dark(oklch(0.8993 0.0309 68.0), oklch(0.2989 0.0277 68.0));
    --warn-6: light-dark(oklch(0.8408 0.0557 68.0), oklch(0.3311 0.0336 68.0));
    --warn-7: light-dark(oklch(0.6229 0.0752 68.0), oklch(0.5314 0.0642 68.0));
    --warn-8: light-dark(oklch(0.5455 0.0791 68.0), oklch(0.6099 0.0884 68.0));
    --warn-9: light-dark(oklch(0.6575 0.1328 68.0), oklch(0.6600 0.1333 68.0));
    --warn-10: light-dark(oklch(0.6153 0.1243 68.0), oklch(0.7038 0.1421 68.0));
    --warn-11: light-dark(oklch(0.5262 0.0717 68.0), oklch(0.6296 0.0857 68.0));
    --warn-12: light-dark(oklch(0.4206 0.0370 68.0), oklch(0.7457 0.0655 68.0));
    --warn-ink: light-dark(oklch(0.1600 0.0000 68.0), oklch(0.1600 0.0000 68.0));

    /* ok */
    --ok-1: light-dark(oklch(0.9800 0.0035 142.0), oklch(0.1750 0.0060 142.0));
    --ok-2: light-dark(oklch(0.9609 0.0114 142.0), oklch(0.2111 0.0115 142.0));
    --ok-3: light-dark(oklch(0.9357 0.0319 142.0), oklch(0.2435 0.0215 142.0));
    --ok-4: light-dark(oklch(0.9131 0.0590 142.0), oklch(0.2700 0.0311 142.0));
    --ok-5: light-dark(oklch(0.8882 0.0992 142.0), oklch(0.2938 0.0417 142.0));
    --ok-6: light-dark(oklch(0.8259 0.1278 142.0), oklch(0.3248 0.0505 142.0));
    --ok-7: light-dark(oklch(0.6084 0.1126 142.0), oklch(0.5191 0.0961 142.0));
    --ok-8: light-dark(oklch(0.5303 0.1178 142.0), oklch(0.5929 0.1317 142.0));
    --ok-9: light-dark(oklch(0.6300 0.1951 142.0), oklch(0.6600 0.2044 142.0));
    --ok-10: light-dark(oklch(0.5895 0.1826 142.0), oklch(0.7030 0.2177 142.0));
    --ok-11: light-dark(oklch(0.5124 0.1070 142.0), oklch(0.6131 0.1280 142.0));
    --ok-12: light-dark(oklch(0.4135 0.0558 142.0), oklch(0.7331 0.0987 142.0));
    --ok-ink: light-dark(oklch(0.1600 0.0000 142.0), oklch(0.1600 0.0000 142.0));

    /* info */
    --info-1: light-dark(oklch(0.9800 0.0070 285.0), oklch(0.1750 0.0070 285.0));
    --info-2: light-dark(oklch(0.9617 0.0070 285.0), oklch(0.2120 0.0070 285.0));
    --info-3: light-dark(oklch(0.9387 0.0070 285.0), oklch(0.2455 0.0070 285.0));
    --info-4: light-dark(oklch(0.9190 0.0070 285.0), oklch(0.2730 0.0070 285.0));
    --info-5: light-dark(oklch(0.8981 0.0070 285.0), oklch(0.2979 0.0070 285.0));
    --info-6: light-dark(oklch(0.8385 0.0070 285.0), oklch(0.3298 0.0070 285.0));
    --info-7: light-dark(oklch(0.6195 0.0070 285.0), oklch(0.5286 0.0070 285.0));
    --info-8: light-dark(oklch(0.5417 0.0070 285.0), oklch(0.6057 0.0070 285.0));
    --info-9: light-dark(oklch(0.5650 0.0070 285.0), oklch(0.6600 0.0070 285.0));
    --info-10: light-dark(oklch(0.5256 0.0070 285.0), oklch(0.7035 0.0070 285.0));
    --info-11: light-dark(oklch(0.5228 0.0070 285.0), oklch(0.6257 0.0070 285.0));
    --info-12: light-dark(oklch(0.4191 0.0070 285.0), oklch(0.7430 0.0070 285.0));
    --info-ink: light-dark(oklch(1.0000 0.0000 285.0), oklch(0.1600 0.0000 285.0));
  }

  /* Forcing a mode is one declaration, not a second copy of the palette. These work
     on any element, which is what makes a dark island on a light page free. */
  .light, [data-theme='light'] { color-scheme: light; }
  .dark,  [data-theme='dark']  { color-scheme: dark; }
}

/* Wide gamut. A separately solved and separately proven palette — see src/css.mjs. */
@media (color-gamut: p3) {
  @layer nilam.tokens {
    :root {
    /* neutral */
    --neutral-1: light-dark(color(display-p3 0.9714 0.97185 0.99076), color(display-p3 0.06296 0.06311 0.07538));
    --neutral-2: light-dark(color(display-p3 0.9475 0.94795 0.96676), color(display-p3 0.0952 0.09539 0.10827));
    --neutral-3: light-dark(color(display-p3 0.91755 0.918 0.9367), color(display-p3 0.12571 0.12593 0.13929));
    --neutral-4: light-dark(color(display-p3 0.89198 0.89242 0.91102), color(display-p3 0.15153 0.15176 0.16548));
    --neutral-5: light-dark(color(display-p3 0.86516 0.86559 0.88409), color(display-p3 0.17552 0.17578 0.1898));
    --neutral-6: light-dark(color(display-p3 0.78933 0.78975 0.80793), color(display-p3 0.20693 0.2072 0.22159));
    --neutral-7: light-dark(color(display-p3 0.52275 0.52312 0.53998), color(display-p3 0.41857 0.41893 0.43512));
    --neutral-8: light-dark(color(display-p3 0.4333 0.43366 0.44995), color(display-p3 0.50674 0.50711 0.52387));
    --neutral-9: light-dark(color(display-p3 0.4598 0.46017 0.47663), color(display-p3 0.57053 0.57092 0.58804));
    --neutral-10: light-dark(color(display-p3 0.41523 0.41558 0.43175), color(display-p3 0.62267 0.62307 0.64046));
    --neutral-11: light-dark(color(display-p3 0.41202 0.41237 0.42852), color(display-p3 0.53001 0.53039 0.54729));
    --neutral-12: light-dark(color(display-p3 0.29889 0.29921 0.31449), color(display-p3 0.67061 0.67101 0.68865));
    --neutral-ink: light-dark(color(display-p3 1 1 1), color(display-p3 0.05176 0.05176 0.05176));
    --surface: light-dark(color(display-p3 1 1 1), color(display-p3 0.1273 0.1273 0.1273));
    --ink-max: light-dark(color(display-p3 0.05176 0.05176 0.05176), color(display-p3 1 1 1));
    --void: light-dark(color(display-p3 0.86758 0.86802 0.88652), color(display-p3 0.01228 0.01229 0.01842));

    /* brand */
    --brand-1: light-dark(color(display-p3 0.97334 0.97341 0.97634), color(display-p3 0.06226 0.06235 0.08082));
    --brand-2: light-dark(color(display-p3 0.94882 0.94905 0.95798), color(display-p3 0.09308 0.09275 0.13036));
    --brand-3: light-dark(color(display-p3 0.91746 0.91799 0.94119), color(display-p3 0.12128 0.11881 0.19317));
    --brand-4: light-dark(color(display-p3 0.89024 0.89107 0.93113), color(display-p3 0.14527 0.13916 0.25187));
    --brand-5: light-dark(color(display-p3 0.86134 0.86241 0.92451), color(display-p3 0.16777 0.15565 0.31394));
    --brand-6: light-dark(color(display-p3 0.78141 0.78237 0.89023), color(display-p3 0.19787 0.18029 0.37928));
    --brand-7: light-dark(color(display-p3 0.50404 0.48813 0.79427), color(display-p3 0.40139 0.36417 0.74846));
    --brand-8: light-dark(color(display-p3 0.41641 0.36442 0.81633), color(display-p3 0.48641 0.45722 0.84035));
    --brand-9: light-dark(color(display-p3 0.44545 0.34988 0.95961), color(display-p3 0.5305 0.48541 0.96659));
    --brand-10: light-dark(color(display-p3 0.40856 0.27129 0.95627), color(display-p3 0.5805 0.55172 0.9704));
    --brand-11: light-dark(color(display-p3 0.39611 0.34361 0.78624), color(display-p3 0.50987 0.4894 0.82959));
    --brand-12: light-dark(color(display-p3 0.28694 0.27069 0.50042), color(display-p3 0.65901 0.65864 0.80862));
    --brand-ink: light-dark(color(display-p3 1 1 1), color(display-p3 0.05176 0.05176 0.05176));

    /* danger */
    --danger-1: light-dark(color(display-p3 0.97636 0.97269 0.97233), color(display-p3 0.075 0.06008 0.05873));
    --danger-2: light-dark(color(display-p3 0.95831 0.94709 0.94598), color(display-p3 0.11869 0.08859 0.08607));
    --danger-3: light-dark(color(display-p3 0.94223 0.91305 0.91021), color(display-p3 0.16978 0.11113 0.10695));
    --danger-4: light-dark(color(display-p3 0.93298 0.88252 0.87769), color(display-p3 0.21588 0.12803 0.12277));
    --danger-5: light-dark(color(display-p3 0.92733 0.84899 0.84168), color(display-p3 0.26265 0.14076 0.13501));
    --danger-6: light-dark(color(display-p3 0.89619 0.75765 0.74563), color(display-p3 0.3144 0.16209 0.15597));
    --danger-7: light-dark(color(display-p3 0.80083 0.39429 0.38507), color(display-p3 0.64634 0.31313 0.30558));
    --danger-8: light-dark(color(display-p3 0.71208 0.28842 0.2898), color(display-p3 0.82642 0.33966 0.34124));
    --danger-9: light-dark(color(display-p3 0.85903 0.15014 0.22908), color(display-p3 0.97066 0.21961 0.2843));
    --danger-10: light-dark(color(display-p3 0.78004 0.13232 0.20449), color(display-p3 0.97346 0.36379 0.37539));
    --danger-11: light-dark(color(display-p3 0.6637 0.28707 0.2845), color(display-p3 0.84403 0.37299 0.36976));
    --danger-12: light-dark(color(display-p3 0.42646 0.24902 0.24004), color(display-p3 0.82164 0.61981 0.60447));
    --danger-ink: light-dark(color(display-p3 1 1 1), color(display-p3 0.05176 0.05176 0.05176));

    /* warn */
    --warn-1: light-dark(color(display-p3 0.97635 0.97318 0.96943), color(display-p3 0.06881 0.06349 0.0572));
    --warn-2: light-dark(color(display-p3 0.95808 0.94835 0.93686), color(display-p3 0.10563 0.09492 0.08226));
    --warn-3: light-dark(color(display-p3 0.94155 0.91613 0.8861), color(display-p3 0.14459 0.12373 0.09899));
    --warn-4: light-dark(color(display-p3 0.93183 0.8877 0.83553), color(display-p3 0.17886 0.14768 0.11056));
    --warn-5: light-dark(color(display-p3 0.92568 0.85682 0.77536), color(display-p3 0.21244 0.16937 0.1178));
    --warn-6: light-dark(color(display-p3 0.89316 0.77097 0.62561), color(display-p3 0.25236 0.19875 0.13432));
    --warn-7: light-dark(color(display-p3 0.63931 0.49797 0.32618), color(display-p3 0.5143 0.39837 0.25746));
    --warn-8: light-dark(color(display-p3 0.55088 0.40628 0.22661), color(display-p3 0.64154 0.47534 0.26884));
    --warn-9: light-dark(color(display-p3 0.75927 0.5011 0.12971), color(display-p3 0.76314 0.50374 0.1306));
    --warn-10: light-dark(color(display-p3 0.69449 0.45683 0.11491), color(display-p3 0.83155 0.55048 0.14621));
    --warn-11: light-dark(color(display-p3 0.51804 0.38849 0.22899), color(display-p3 0.66217 0.50009 0.30057));
    --warn-12: light-dark(color(display-p3 0.35225 0.28967 0.21489), color(display-p3 0.778 0.6502 0.49747));
    --warn-ink: light-dark(color(display-p3 0.05176 0.05176 0.05176), color(display-p3 0.05176 0.05176 0.05176));

    /* ok */
    --ok-1: light-dark(color(display-p3 0.96985 0.9764 0.96794), color(display-p3 0.05975 0.06756 0.05747));
    --ok-2: light-dark(color(display-p3 0.93607 0.95725 0.9299), color(display-p3 0.08644 0.10204 0.08189));
    --ok-3: light-dark(color(display-p3 0.88007 0.93933 0.86281), color(display-p3 0.10658 0.13672 0.09777));
    --ok-4: light-dark(color(display-p3 0.81916 0.92885 0.78717), color(display-p3 0.12176 0.16657 0.10863));
    --ok-5: light-dark(color(display-p3 0.73761 0.92276 0.68341), color(display-p3 0.13335 0.19499 0.1152));
    --ok-6: light-dark(color(display-p3 0.61597 0.86085 0.54364), color(display-p3 0.15376 0.23034 0.13113));
    --ok-7: light-dark(color(display-p3 0.37838 0.57971 0.31823), color(display-p3 0.30033 0.4655 0.25099));
    --ok-8: light-dark(color(display-p3 0.28282 0.48894 0.21966), color(display-p3 0.33346 0.57036 0.26087));
    --ok-9: light-dark(color(display-p3 0.26855 0.64501 0.12344), color(display-p3 0.28966 0.69061 0.13515));
    --ok-10: light-dark(color(display-p3 0.24275 0.5893 0.10914), color(display-p3 0.31798 0.75178 0.15084));
    --ok-11: light-dark(color(display-p3 0.27831 0.46287 0.22239), color(display-p3 0.36226 0.59314 0.2923));
    --ok-12: light-dark(color(display-p3 0.23714 0.32669 0.21081), color(display-p3 0.54275 0.7256 0.48899));
    --ok-ink: light-dark(color(display-p3 0.05176 0.05176 0.05176), color(display-p3 0.05176 0.05176 0.05176));

    /* info */
    --info-1: light-dark(color(display-p3 0.9714 0.97185 0.99076), color(display-p3 0.06296 0.06311 0.07538));
    --info-2: light-dark(color(display-p3 0.9475 0.94795 0.96676), color(display-p3 0.0952 0.09539 0.10827));
    --info-3: light-dark(color(display-p3 0.91755 0.918 0.9367), color(display-p3 0.12571 0.12593 0.13929));
    --info-4: light-dark(color(display-p3 0.89198 0.89242 0.91102), color(display-p3 0.15153 0.15176 0.16548));
    --info-5: light-dark(color(display-p3 0.86516 0.86559 0.88409), color(display-p3 0.17552 0.17578 0.1898));
    --info-6: light-dark(color(display-p3 0.78933 0.78975 0.80793), color(display-p3 0.20693 0.2072 0.22159));
    --info-7: light-dark(color(display-p3 0.52275 0.52312 0.53998), color(display-p3 0.41857 0.41893 0.43512));
    --info-8: light-dark(color(display-p3 0.4333 0.43366 0.44995), color(display-p3 0.50674 0.50711 0.52387));
    --info-9: light-dark(color(display-p3 0.4598 0.46017 0.47663), color(display-p3 0.57053 0.57092 0.58804));
    --info-10: light-dark(color(display-p3 0.41523 0.41558 0.43175), color(display-p3 0.62267 0.62307 0.64046));
    --info-11: light-dark(color(display-p3 0.41202 0.41237 0.42852), color(display-p3 0.53001 0.53039 0.54729));
    --info-12: light-dark(color(display-p3 0.29889 0.29921 0.31449), color(display-p3 0.67061 0.67101 0.68865));
    --info-ink: light-dark(color(display-p3 1 1 1), color(display-p3 0.05176 0.05176 0.05176));
    }
  }
}

/* ── nilam.motion.css ────────────────────────────────────────────────── */

/* nilam — essential motion.
 *
 * One block, in the FIRST cascade layer, and the layer position is the entire reason this file
 * exists on its own.
 *
 * nilam.base carries the usual blanket reduced-motion rule:
 *
 *   @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important } }
 *
 * which is right for decoration and wrong for a loader. WCAG 2.3.3 governs NON-ESSENTIAL
 * animation, and a loading indicator is essential by definition: frozen, it does not read as
 * "motion reduced", it reads as "this app has hung", and the reader loses the only signal that
 * anything is still happening.
 *
 * This exemption first lived in nilam.components, one layer after base, on the reasonable
 * assumption that a later layer wins. IT DOES NOT, FOR !IMPORTANT DECLARATIONS — that order is
 * reversed and earlier layers win — so base beat it, every loader froze for three releases, and
 * the comment beside it confidently described the opposite. Measured on a built page: spinner,
 * bar and skeleton all reported `n-breathe 1e-05s 1 iteration`.
 *
 * Hence nilam.motion, declared first. What these keep is the one property that carries the
 * signal: opacity, which moves nothing across the screen and does not provoke vestibular
 * symptoms. The travelling bar becomes a pulse in place; the spinner breathes instead of
 * rotating.
 */

@layer nilam.motion {
/* ── loaders under prefers-reduced-motion ────────────────────────────────
 *
 * The base layer's blanket rule sets animation-duration to 0.01ms on everything, which
 * for a loader is actively harmful: a FROZEN spinner does not read as "reduced motion",
 * it reads as "this app has hung". The reader loses the only signal that anything is
 * still happening.
 *
 * WCAG 2.3.3 governs non-essential animation, and a loading indicator is essential by
 * definition, so an exemption is correct rather than a loophole. What these keep is the
 * ONE property that carries the signal — opacity, which does not move anything across
 * the screen and does not trigger vestibular symptoms. The travelling bar becomes a
 * pulse in place; the spinner stops rotating and breathes instead.
 */
@media (prefers-reduced-motion: reduce) {
  .n-spinner,
  .n-dots i,
  .n-bar::after,
  .n-btn[aria-busy='true']::before {
    animation-duration: 1.4s !important;
    animation-iteration-count: infinite !important;
  }
  .n-spinner, .n-btn[aria-busy='true']::before {
    animation-name: n-breathe !important;
    /* Without rotation the single-arc trick stops reading as a ring, so close it. */
    border-block-start-color: var(--brand-9);
  }
  .n-bar::after {
    animation-name: n-breathe !important;
    inline-size: 100%;
    translate: none;
  }
  /* iteration-count as well as duration, and its absence was a second bug hiding inside the
   * first. The skeleton was not in the selector list above, so it inherited base's
   * `animation-iteration-count: 1 !important` and breathed exactly once before stopping —
   * which for a skeleton is worse than not animating, because the reader watches it settle
   * and concludes the content has arrived. Measured: `n-breathe 1.8s 1` after the layer move
   * fixed the other three. */
  .n-skeleton {
    animation-name: n-breathe !important;
    animation-duration: 1.8s !important;
    animation-iteration-count: infinite !important;
  }
}
@keyframes n-breathe { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
}

/* ── nilam.scale.css ─────────────────────────────────────────────────── */

/* nilam — the scales. Everything that is not a colour.
 *
 * Hand-authored, because none of it is solvable the way colour is. There is no contrast
 * requirement to invert for "how big is a heading". What I can do is state where each
 * number comes from, so the next person can argue with the reasoning instead of
 * guessing at the intent.
 */

@layer nilam.tokens {
  :root {
    /* ── type ──────────────────────────────────────────────────────────────
     *
     * Fluid between a 360px and a 1280px viewport, on a ratio that is NOT constant — and
     * saying "a 1.2 minor third", as this comment did, was wrong. Measured at the 1280px end,
     * adjacent steps run 1.07-1.08 below --text-1 and widen to 1.27-1.33 above --text-3. 1.2
     * describes the middle and nothing else.
     *
     * That is deliberate, not drift. A dense UI needs four or five distinct sizes inside one
     * card, which only works if the small end is finely spaced; display sizes need to separate
     * hard or they read as the same size at a glance. A single ratio cannot do both, and the
     * fashionable 1.25 or 1.333 look better in a specimen and run out of room by the third
     * step.
     *
     * clamp() on every step, so there is no breakpoint at which type jumps. The rem
     * units mean a user's own font-size setting still scales all of it — 1.4.4. */
    --text-000: clamp(0.6875rem, 0.67rem + 0.09vw, 0.75rem);   /* micro: labels, legal */
    --text-00:  clamp(0.75rem, 0.73rem + 0.11vw, 0.8125rem);   /* small: captions, meta */
    --text-0:   clamp(0.8125rem, 0.79rem + 0.12vw, 0.875rem);  /* secondary body */
    --text-1:   clamp(0.875rem, 0.85rem + 0.13vw, 0.9375rem);  /* body — the default */
    --text-2:   clamp(1rem, 0.96rem + 0.2vw, 1.125rem);
    --text-3:   clamp(1.125rem, 1.06rem + 0.32vw, 1.375rem);
    --text-4:   clamp(1.375rem, 1.26rem + 0.57vw, 1.75rem);
    --text-5:   clamp(1.625rem, 1.42rem + 1.02vw, 2.25rem);
    --text-6:   clamp(2rem, 1.6rem + 2vw, 3rem);
    --text-7:   clamp(2.5rem, 1.8rem + 3.5vw, 4rem);
    /* The display size, for one thing per page. Weight 200 at this size, never 700:
       large type gets its presence from size, and adding weight to both is why
       "bold and big" reads as shouting rather than as confidence. */
    --text-display: clamp(3rem, 1.6rem + 7vw, 7.5rem);

    --font-sans:
      system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
      'Apple Color Emoji', 'Segoe UI Emoji';
    --font-mono:
      ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;

    /* 200 exists because --text-display's own comment says the display size wants weight
       200, and the first version of this file then shipped nothing below 400 — so the one
       treatment the scale explicitly recommends was inexpressible with its own tokens.
       Found by an app that had to define --weight-thin locally to follow the advice. */
    --weight-thin: 200;
    --weight-light: 300;
    --weight-normal: 400;
    --weight-medium: 500;
    --weight-semibold: 600;
    --weight-bold: 700;

    /* Line height falls as size rises. A single value cannot serve both: 1.6 on a
       display heading looks like a gap, 1.2 on body text is unreadable. */
    --leading-tight: 1.15;
    --leading-snug: 1.35;
    --leading-normal: 1.6;
    --leading-loose: 1.75;

    /* Tracking rises as size falls, which is the same rule in the other direction.
       Micro text at 0 tracking looks cramped; that is why every good UI sets its
       10px uppercase labels wide. */
    --tracking-tight: -0.02em;
    --tracking-normal: 0;
    --tracking-wide: 0.04em;
    --tracking-micro: 0.14em;

    /* 65ch is the measure at which prose stops being tiring to read. Not a preference —
       it is the one typographic number with a real literature behind it. */
    --measure: 65ch;

    /* ── space ─────────────────────────────────────────────────────────────
     *
     * A 4px grid, in rem so it scales with the user's font size. Doubling from
     * space-3 up: small increments matter inside a control, and past 1rem the eye
     * cannot tell 20px from 24px anyway, so offering both only invites inconsistency. */
    --space-0: 0.125rem;   /*  2px */
    --space-1: 0.25rem;    /*  4px */
    --space-2: 0.5rem;     /*  8px */
    --space-3: 0.75rem;    /* 12px */
    --space-4: 1rem;       /* 16px */
    --space-5: 1.5rem;     /* 24px */
    --space-6: 2rem;       /* 32px */
    --space-7: 3rem;       /* 48px */
    --space-8: 4rem;       /* 64px */
    --space-9: 6rem;       /* 96px */

    /* ── radius ────────────────────────────────────────────────────────────
     *
     * Radii must nest correctly or the corners look wrong in a way people notice but
     * cannot name: an inner radius should be the outer minus the padding between them.
     * Hence a scale with small gaps at the bottom, where nesting actually happens. */
    --r-1: 0.25rem;
    --r-2: 0.375rem;
    --r-3: 0.5rem;
    --r-4: 0.625rem;
    --r-5: 0.875rem;
    --r-6: 1.25rem;
    --r-full: 9999px;

    /* ── lines ─────────────────────────────────────────────────────────────
     *
     * 1px, but as a token, because a hairline on a 3x display should not be 3 device
     * pixels. Kept separate from radius so a theme can go borderless without touching
     * anything else. */
    --line-1: 1px;
    --line-2: 2px;

    /* ── motion ────────────────────────────────────────────────────────────
     *
     * Short. 150ms is roughly where a UI transition stops reading as "responding" and
     * starts reading as "loading". The long ones are only for things that travel a
     * distance — a drawer, a dialog. */
    --dur-0: 80ms;
    --dur-1: 150ms;
    --dur-2: 240ms;
    --dur-3: 400ms;

    /* ease-out for anything entering (fast then settling reads as arriving), and the
       spring for anything the user directly caused, which is where a little overshoot
       reads as physical rather than as decoration. */
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
    --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);

    /* ── focus ─────────────────────────────────────────────────────────────
     *
     * 2px, offset 2px. The offset is not cosmetic: a ring drawn flush against a filled
     * button of similar lightness is invisible, and 1.4.11 wants the ring to have 3:1
     * against ADJACENT colours. The gap guarantees the adjacent colour is the page. */
    --ring-width: 2px;
    --ring-offset: 2px;

    /* ── elevation ─────────────────────────────────────────────────────────
     *
     * The alphas differ by an order of magnitude between modes, and this is the single
     * least obvious thing in the file.
     *
     * Browsers composite alpha in GAMMA-ENCODED sRGB, not linear. So black at 8% over a
     * near-white page darkens it a lot, and the same black at 8% over a near-black page
     * changes almost nothing — the encoding has already spent most of its precision at
     * the dark end. Measured, the gap needed to LOOK equal is about 10x — measured as the alpha
     * that produces the same OKLab lightness drop over each ground: 10.5x at a subtle step,
     * falling to 7.9x at a heavy one. An earlier version of this comment said 23x, which no
     * metric I can construct reproduces; the shipped alphas below are 11x and consistent.
     *
     * Which is why "our dark mode shadows look flat" is a universal complaint and why
     * dark themes that work switch to a light rim instead. Both are here: a shadow, and
     * a top rim that only exists in dark mode, where it does the job the shadow cannot. */
    --shadow-colour: light-dark(oklch(0 0 0), oklch(0 0 0));
    --shadow-1:
      0 1px 2px light-dark(oklch(0 0 0 / 0.05), oklch(0 0 0 / 0.55)),
      0 1px 1px light-dark(oklch(0 0 0 / 0.03), oklch(0 0 0 / 0.4));
    --shadow-2:
      0 1px 2px light-dark(oklch(0 0 0 / 0.04), oklch(0 0 0 / 0.5)),
      0 8px 20px -6px light-dark(oklch(0 0 0 / 0.09), oklch(0 0 0 / 0.6));
    --shadow-3:
      0 2px 4px light-dark(oklch(0 0 0 / 0.04), oklch(0 0 0 / 0.5)),
      0 16px 40px -12px light-dark(oklch(0 0 0 / 0.14), oklch(0 0 0 / 0.7));

    /* The rim. inset, top only, and transparent in light mode where it would read as a
       scratch. In dark mode it is what makes a raised surface look raised. */
    --rim: inset 0 1px 0 light-dark(transparent, oklch(1 0 0 / 0.06));

    /* Dialog backdrop. The light alpha is LOWER (0.4 against 0.6) and its effect is heavier:
       measured, it drops the page by ΔL 0.283 where the dark one drops 0.052. Same reason the
       shadows differ — alpha composites on gamma-encoded bytes, so the light end has far more
       of them to spend. An earlier version of this comment said "heavier in light mode", which
       is true of the effect and reads as a claim about the alpha, which is the other way up. */
    --scrim: light-dark(oklch(0.2 0.01 285 / 0.4), oklch(0 0 0 / 0.6));

    /* ── z ────────────────────────────────────────────────────────────────
     *
     * Only four, and three of them are legacy. Dialogs and popovers now live in the
     * browser's top layer, which no z-index can reach, so the stacking bugs these
     * numbers used to solve mostly do not exist any more. */
    --z-base: 1;
    --z-sticky: 10;
    --z-overlay: 100;
    --z-max: 1000;
  }
}

/* ── nilam.base.css ──────────────────────────────────────────────────── */

/* nilam — base layer. Element defaults, and the hue leaks closed.
 *
 * Not a reset. normalize/preflight already exist and are better at it than anything I
 * would write. This does the one thing a colour system has to do at the element level:
 * stop the browser from painting a hue nobody chose.
 *
 * Every rule below started as a bug I found by screenshotting a page that had no colour
 * in its stylesheet at all and still was not achromatic. The leaks, in the order I
 * found them: link blue, focus-ring blue, ::selection blue, form-control accent blue,
 * <mark> yellow, <hr> grey, spinner/progress blue, and — the one that took longest —
 * the UA's own text-decoration colour on <a>, which is not `currentColor` by default.
 */

@layer nilam.base {
  *, *::before, *::after { box-sizing: border-box; }

  html {
    /* 1.4.4 Resize Text: never set a px root size. */
    -webkit-text-size-adjust: 100%;
    /* Only ever tab-scroll to a heading that is not glued to the viewport edge. */
    scroll-padding-block-start: 4rem;
  }

  body {
    margin: 0;
    background: var(--neutral-1);
    color: var(--neutral-12);
    font-family: var(--font-sans);
    font-size: var(--text-1);
    line-height: var(--leading-normal);
    /* Antialiasing is a taste call, and on macOS the default is heavier than the
       designs anyone draws. Kept because every reference interface I measured uses it. */
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-rendering: optimizeLegibility;
  }

  /* LEAK 1: link blue. `color: inherit` alone is not enough — the UA also paints the
     underline in its own blue, and text-decoration-color does not inherit from color
     unless you say so. */
  a {
    color: inherit;
    text-decoration-line: underline;
    text-decoration-thickness: from-font;
    text-decoration-color: var(--neutral-8);
    text-underline-offset: 0.15em;
    transition: text-decoration-color var(--dur-1) var(--ease-out);
  }
  a:hover { text-decoration-color: currentColor; }

  /* LEAK 2: the focus ring. Two rules, not one. :focus-visible styles the ring the way
     we want; the bare :focus reset removes the UA ring for mouse users. Doing only the
     first leaves Safari drawing its own ring underneath ours. */
  :focus { outline: none; }
  :focus-visible {
    outline: var(--ring-width) solid var(--brand-9);
    outline-offset: var(--ring-offset);
    /* A ring on a dark element against a dark page needs a gap it can be seen in. */
    border-radius: inherit;
  }

  /* LEAK 3: selection. Uses the solid + its solved ink, so it is legible in both modes
     rather than only the one the author happened to be looking at. */
  ::selection {
    background: var(--brand-9);
    color: var(--brand-ink);
  }

  /* LEAK 4: form controls paint their checkmarks, ticks and thumbs in the OS accent. */
  :where(input, select, textarea, button, progress, meter) { accent-color: var(--brand-9); }

  /* LEAK 5: <mark> is UA yellow, which is a hue AND usually fails contrast. */
  mark {
    background: var(--warn-3);
    color: var(--warn-12);
    border-radius: var(--r-1);
    padding-inline: 0.15em;
  }

  /* Controls do not inherit the page font unless told to — the oldest bug in CSS. */
  button, input, select, textarea, optgroup { font: inherit; letter-spacing: inherit; }
  button { color: inherit; }

  code, kbd, samp, pre {
    font-family: var(--font-mono);
    /* 0.9375em not 0.875em: monospace faces run visually larger at the same size, and
       0.875 made inline code read a step smaller than its surrounding text. */
    font-size: 0.9375em;
  }
  pre { overflow-x: auto; }

  hr {
    border: 0;
    border-block-start: var(--line-1) solid var(--neutral-6);
    margin-block: var(--space-5);
  }

  /* Media should not be able to overflow its column. width:auto with the max-inline
     keeps intrinsic sizing intact, which `width:100%` destroys for SVG. */
  :where(img, svg, video, canvas, audio, iframe, embed, object) {
    display: block;
    max-inline-size: 100%;
  }
  :where(img, svg, video, canvas) { block-size: auto; }

  /* Headings inherit; sizing them is the type scale's job, not the reset's. */
  :where(h1, h2, h3, h4, h5, h6) {
    margin: 0;
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    /* Optical tightening: large type at default tracking reads loose. */
    letter-spacing: -0.02em;
    text-wrap: balance;
  }
  :where(p, ul, ol, dl, blockquote, figure, pre) { margin: 0; }
  /* Stop a paragraph ending on a single orphaned word. */
  :where(p, li) { text-wrap: pretty; }

  :where(ul, ol) { padding-inline-start: 0; list-style: none; }

  table { border-collapse: collapse; border-spacing: 0; }
  :where(th, td) { text-align: start; }

  /* [hidden] must beat any author display. Without !important, `.flex { display:flex }`
     silently un-hides a hidden element — measured, in a real app, and the reason this
     is the only !important in the package. */
  [hidden] { display: none !important; }

  /* Honour the OS setting. Not `animation: none`, which breaks JS that waits for
     animationend; 0.01ms fires the event and shows no motion. */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }
}

/* ── nilam.components.css ────────────────────────────────────────────── */

/* nilam — components.
 *
 * ── the scope, stated honestly ────────────────────────────────────────────────
 *
 * This does not compete with Radix or React Aria on breadth and it never will. React
 * Aria is years of screen-reader, touch and IME work by a team; a stylesheet cannot
 * contain that. What a stylesheet CAN do in 2026 is much more than it could in 2020,
 * because the platform grew the hard parts:
 *
 *   <dialog>              modal, focus trap, Esc to close, inert background, ::backdrop
 *   popover / popovertarget   top layer, light dismiss, Esc, implicit anchor reference
 *   CSS anchor positioning    Baseline 2026 — tooltips and menus without Floating UI
 *   <details name>        exclusive accordion, no JS at all
 *   :has()                parent selectors, so a field can style itself from its input
 *   field-sizing          textareas that grow, without a scroll-height listener
 *
 * So every overlay below is native. The dialog needs no focus-trap library, the menu
 * needs no positioning library, the accordion needs no state. What is left for JS is
 * genuinely stateful widgets — combobox, date picker, virtualised table — and those are
 * deliberately absent rather than badly approximated. Use React Aria for them. Pairing
 * is the honest recommendation, not a fallback.
 *
 * ── the rule every status component follows ───────────────────────────────────
 *
 * prove.mjs measures which status colours collapse under each dichromacy and, at hue
 * 285, reports that danger/warn, danger/ok and warn/ok all become indistinguishable to
 * a deuteranope. So every status component here carries a GLYPH as well as a hue, and
 * proveStatusChannels() fails the build if one does not. WCAG 1.4.1 has required this
 * for years; this is the first system I know of that enforces it.
 */

@layer nilam.components {
  /* ── button ─────────────────────────────────────────────────────────────── */

  .n-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    /* Padding is asymmetric on purpose: optical centring. Equal block padding on a
       control with a cap-height-only label reads bottom-heavy. */
    padding: 0.5rem 0.875rem;
    border: var(--line-1) solid var(--neutral-7);
    border-radius: var(--r-3);
    background: transparent;
    color: var(--neutral-12);
    font-size: var(--text-1);
    font-weight: var(--weight-medium);
    line-height: 1;
    /* 44px is the WCAG 2.5.8 target-size minimum. The control is 36px tall so it does
       not look chunky, and the shortfall is made up by a transparent pseudo-element —
       the pointer target is compliant while the painted box stays the size the design
       wants. */
    min-block-size: 2.25rem;
    position: relative;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    user-select: none;
    transition:
      background var(--dur-1) var(--ease-out),
      border-color var(--dur-1) var(--ease-out),
      box-shadow var(--dur-1) var(--ease-out);
  }

  /* The hit-target extension. ::after rather than padding, so layout is untouched. */
  .n-btn::after {
    content: '';
    position: absolute;
    inset: 50% 0 auto;
    translate: 0 -50%;
    block-size: max(100%, 2.75rem);
  }

  /* The tint is not the state in dark mode: --neutral-3 on --surface is 1.0009:1, because
     dark --surface is step 3's lightness with the tint taken off, so the two differ by
     chroma alone. This button already owns a --neutral-7 border, so the cheapest honest
     second channel is to move that border rather than add a ring — --neutral-8 against
     --neutral-7 is a full solved step, and it costs no layout. */
  .n-btn:hover { background: var(--neutral-3); border-color: var(--neutral-8); }
  .n-btn:active { background: var(--neutral-4); border-color: var(--neutral-8); }
  .n-btn:disabled,
  .n-btn[aria-disabled='true'] {
    /* Not opacity. Fading a button fades its label too and drops it below 4.5:1 — the
       most common accessibility bug in every component library I checked. Disabled state
       is expressed with tokens that are still legible. */
    background: var(--neutral-2);
    border-color: var(--neutral-6);
    color: var(--neutral-11);
    cursor: not-allowed;
  }

  /* The brand moment. Step 9 flips polarity by mode and --brand-ink follows it, so this
     one rule is a dark button with white text on a light page and a glowing light button
     with dark text on a dark page. Writing `color: #fff` here would break dark mode. */
  .n-btn-fill {
    background: var(--brand-9);
    border-color: transparent;
    color: var(--brand-ink);
    box-shadow: var(--shadow-1);
  }
  .n-btn-fill:hover { background: var(--brand-10); }
  .n-btn-fill:active { background: var(--brand-10); box-shadow: none; }

  .n-btn-ink {
    background: var(--neutral-12);
    border-color: transparent;
    color: var(--neutral-1);
  }
  .n-btn-ink:hover { background: var(--neutral-11); }

  .n-btn-ghost { border-color: transparent; }
  /* A ghost button has given its border away, so the border trick above is not available
     and it needs the ring instead. --neutral-6, not --neutral-7: this is pointer
     affordance, which 1.4.11 does not govern, and a 3:1 ring would make a ghost button
     look bordered on hover — which is the thing it exists not to be. */
  .n-btn-ghost:hover {
    background: var(--neutral-3);
    box-shadow: inset 0 0 0 var(--line-1) var(--neutral-6);
  }

  .n-btn-danger {
    background: var(--danger-9);
    border-color: transparent;
    color: var(--danger-ink);
  }
  .n-btn-danger:hover { background: var(--danger-10); }

  /* ok and warn fills existed for badges, notes and meters but not for BUTTONS, so an app with
   * a matched affirmative/negative pair — a Yes/No vote, an approve/reject row — had to
   * hand-roll one of the two and lost the solved ink with it. Note the inks differ: at hue 285
   * the ok solid takes dark ink in both modes while danger flips, so hard-coding either is
   * wrong on one of them. */
  .n-btn-ok {
    background: var(--ok-9);
    border-color: transparent;
    color: var(--ok-ink);
  }
  .n-btn-ok:hover { background: var(--ok-10); }
  .n-btn-warn {
    background: var(--warn-9);
    border-color: transparent;
    color: var(--warn-ink);
  }
  .n-btn-warn:hover { background: var(--warn-10); }

  .n-btn-sm { padding: 0.3125rem 0.625rem; font-size: var(--text-0); min-block-size: 1.75rem; }
  .n-btn-lg { padding: 0.6875rem 1.125rem; font-size: var(--text-2); min-block-size: 2.75rem; }
  .n-btn-icon { padding: 0; inline-size: 2.25rem; }
  .n-btn-icon.n-btn-sm { inline-size: 1.75rem; }

  /* Loading. aria-busy is the accessible signal; the spinner is the visible one.
   *
   * The label goes transparent rather than being removed, so the button keeps its width
   * and the row does not reflow when it starts loading. */
  .n-btn[aria-busy='true'] { color: transparent; pointer-events: none; }
  .n-btn[aria-busy='true']::before {
    content: '';
    position: absolute;
    inline-size: 0.875rem;
    block-size: 0.875rem;
    /* border-color, NOT `color`, and this is the bug the first version had. `color` was set
     * to --brand-ink on the base rule, which is near-white in light mode — so a ghost or
     * outline button's spinner was white on a transparent background and simply invisible.
     * Each variant now names its own visible colour. */
    border: var(--line-2) solid var(--neutral-12);
    border-block-start-color: transparent;
    border-radius: var(--r-full);
    animation: n-spin 0.6s linear infinite;
  }
  .n-btn-fill[aria-busy='true']::before,
  .n-btn-danger[aria-busy='true']::before,
  .n-btn-ok[aria-busy='true']::before,
  .n-btn-warn[aria-busy='true']::before { border-color: var(--brand-ink); border-block-start-color: transparent; }

  /* Disabled AND busy at once, which is what a form does the instant it submits. The :disabled
   * rule wins the background on specificity while the spinner still took --brand-ink, so the
   * ring was ~1.09:1 on the disabled surface — invisible exactly when it is the only feedback
   * the user has. Named last so it beats the variant rules above. */
  .n-btn:disabled[aria-busy='true']::before,
  .n-btn[aria-disabled='true'][aria-busy='true']::before {
    border-color: var(--neutral-11);
    border-block-start-color: transparent;
  }
  .n-btn-ink[aria-busy='true']::before { border-color: var(--neutral-1); border-block-start-color: transparent; }

  /* ── loaders ─────────────────────────────────────────────────────────────
   *
   * Four, because they answer four different questions and substituting one for another is
   * a real usability error:
   *
   *   .n-spinner   "something is happening"       — unknown duration, small, inline
   *   .n-progress  "this much is done"            — known proportion, uses <progress>
   *   .n-bar       "something is happening"       — unknown duration, page or panel width
   *   .n-skeleton  "content shaped like THIS is coming"
   *
   * Prefer .n-skeleton wherever the shape is predictable. A spinner tells the reader
   * nothing about what is arriving; a skeleton tells them where to look when it does.
   */

  .n-spinner {
    display: inline-block;
    flex: none;
    inline-size: 1.125rem;
    block-size: 1.125rem;
    border: var(--line-2) solid var(--neutral-6);
    /* One arc in a stronger colour is what makes rotation legible. A ring of uniform
       colour rotating looks static. */
    border-block-start-color: var(--brand-9);
    border-radius: var(--r-full);
    animation: n-spin 0.6s linear infinite;
  }
  .n-spinner-sm { inline-size: 0.875rem; block-size: 0.875rem; border-width: var(--line-1); }
  .n-spinner-lg { inline-size: 1.75rem; block-size: 1.75rem; }
  /* On a filled surface the ring must come from the ink, not from the neutral ramp. */
  .n-spinner-on-fill { border-color: color-mix(in oklab, var(--brand-ink) 30%, transparent); border-block-start-color: var(--brand-ink); }

  @keyframes n-spin { to { rotate: 360deg; } }

  /* Determinate. The element is <progress>, so the value is in the accessibility tree for
     free and no aria-valuenow needs maintaining. appearance:none then rebuilds it, and
     both pseudo-element sets are needed because WebKit and Firefox disagree. */
  .n-progress {
    appearance: none;
    inline-size: 100%;
    block-size: 0.375rem;
    /* See .n-meter: without this the empty portion is 1.08:1 on a card in dark. */
    border: var(--line-1) solid var(--neutral-7);
    border-radius: var(--r-full);
    background: var(--neutral-4);
    overflow: hidden;
  }
  .n-progress::-webkit-progress-bar { background: var(--neutral-4); border-radius: var(--r-full); }
  .n-progress::-webkit-progress-value {
    background: var(--brand-9);
    border-radius: var(--r-full);
    transition: inline-size var(--dur-2) var(--ease-out);
  }
  .n-progress::-moz-progress-bar { background: var(--brand-9); border-radius: var(--r-full); }

  /* Indeterminate bar. A <progress> with no value attribute is already indeterminate to
     assistive tech, so this styles that state rather than inventing a widget. */
  .n-bar {
    position: relative;
    inline-size: 100%;
    block-size: 0.1875rem;
    border: var(--line-1) solid var(--neutral-7);
    border-radius: var(--r-full);
    background: var(--neutral-4);
    overflow: hidden;
  }
  .n-bar::after {
    content: '';
    position: absolute;
    inset-block: 0;
    inline-size: 40%;
    border-radius: inherit;
    background: var(--brand-9);
    animation: n-slide 1.4s var(--ease-in-out) infinite;
  }
  @keyframes n-slide {
    0%   { translate: -110% 0; }
    100% { translate: 260% 0; }
  }

  /* Three dots, for inline "typing"/thinking states where a ring is too heavy. */
  .n-dots { display: inline-flex; gap: 0.25rem; align-items: center; }
  .n-dots i {
    inline-size: 0.3125rem;
    block-size: 0.3125rem;
    border-radius: var(--r-full);
    background: var(--neutral-11);
    animation: n-pulse 1.2s var(--ease-in-out) infinite;
  }
  .n-dots i:nth-child(2) { animation-delay: 0.15s; }
  .n-dots i:nth-child(3) { animation-delay: 0.3s; }
  @keyframes n-pulse {
    0%, 60%, 100% { opacity: 0.3; scale: 0.8; }
    30%           { opacity: 1;   scale: 1; }
  }

  /* The reduced-motion exemption for loaders is NOT here. It lives in nilam.motion, the FIRST
   * layer, because !important declarations resolve in REVERSE layer order — a rule in this
   * layer therefore loses to nilam.base's blanket `animation-duration: 0.01ms !important` on *.
   * It sat here for three releases and every loader froze. See nilam.motion.css. */

  /* The accessible wrapper. role="status" has an implicit aria-live="polite", so the text
     inside is announced once without interrupting. The visible loader is decorative next
     to it, which is why examples pair them. */
  .n-loading {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-0);
    color: var(--neutral-11);
  }

  /* ── field ──────────────────────────────────────────────────────────────── */

  .n-field { display: flex; flex-direction: column; gap: var(--space-1); }

  .n-label {
    font-size: var(--text-0);
    font-weight: var(--weight-medium);
    color: var(--neutral-12);
  }
  /* The required marker is a hue AND a glyph, and it is aria-hidden in markup because
     the input's own `required` is what assistive tech should read. */
  .n-label[data-required]::after {
    content: ' *';
    color: var(--danger-11);
  }

  .n-input,
  .n-textarea,
  .n-select {
    inline-size: 100%;
    padding: 0.5rem 0.6875rem;
    border: var(--line-1) solid var(--neutral-7);
    border-radius: var(--r-3);
    background: var(--neutral-1);
    color: var(--neutral-12);
    font-size: var(--text-1);
    min-block-size: 2.25rem;
    transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out);
  }
  .n-input::placeholder,
  .n-textarea::placeholder {
    /* Step 11, not a lighter grey. Placeholder text is text and 1.4.3 applies to it;
       most systems ship placeholders around 2.5:1. */
    color: var(--neutral-11);
  }
  .n-input:hover, .n-textarea:hover, .n-select:hover { border-color: var(--neutral-8); }
  .n-input:focus-visible, .n-textarea:focus-visible, .n-select:focus-visible {
    outline: none;
    border-color: var(--brand-9);
    /* Ring drawn as a shadow rather than an outline so it hugs the border radius and
       does not clip inside a scroll container. */
    box-shadow: 0 0 0 var(--ring-width) var(--brand-6);
  }

  /* Invalid, but only once the user has finished — :invalid alone paints an empty
     required field red before it has been touched, which is hostile. user-invalid is
     the correct selector and it is Baseline. */
  .n-input:user-invalid, .n-textarea:user-invalid, .n-select:user-invalid,
  .n-input[aria-invalid='true'], .n-textarea[aria-invalid='true'], .n-select[aria-invalid='true'] {
    border-color: var(--danger-8);
  }
  .n-input:user-invalid:focus-visible,
  .n-input[aria-invalid='true']:focus-visible { box-shadow: 0 0 0 var(--ring-width) var(--danger-6); }

  /* [aria-invalid] is not a duplicate of :user-invalid, it is the case :user-invalid cannot
   * reach. :user-invalid only fires for constraints the BROWSER evaluates — required, type,
   * pattern. An application-level rule ("that is not a magnet URI", "this username is taken")
   * has no browser constraint to violate, so on a plain valid text field :user-invalid never
   * matches and the border stayed neutral while the error text below it said otherwise.
   * Since aria-invalid is the attribute assistive tech reads anyway, styling it means the
   * visual state and the announced state cannot disagree. */

  .n-textarea {
    /* Grows with content where supported, and stays a normal resizable textarea where
       not. Pure enhancement — nothing breaks without it. */
    field-sizing: content;
    min-block-size: 4.5rem;
    line-height: var(--leading-normal);
    resize: vertical;
  }

  /* The select arrow, drawn in currentColor so it follows the mode. A hard-coded SVG
     fill is the usual approach and it is invisible in dark mode. */
  .n-select {
    appearance: none;
    padding-inline-end: 2rem;
    background-image:
      linear-gradient(45deg, transparent 50%, currentColor 50%),
      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position:
      calc(100% - 1.125rem) calc(50% + 0.1em),
      calc(100% - 0.75rem) calc(50% + 0.1em);
    background-size: 0.375rem 0.375rem;
    background-repeat: no-repeat;
  }

  /* Help and error text, wired with aria-describedby in markup. The error carries a
     glyph because danger is one of the colours the prover says collapses. */
  .n-hint { font-size: var(--text-00); color: var(--neutral-11); }
  /* NOT flex, for the same reason .n-summary is not — and this one survived the fix there.
   *
   * Flex makes every text node AND every inline element its own flex item, so the gap opens
   * a hole either side of any <code>, <strong> or <a> mid-sentence. The comment on
   * .n-summary warns about exactly this trap and this component sat one screenful away
   * still falling into it.
   *
   * Block layout with the glyph as an inline ::before keeps the message as prose. */
  .n-error {
    display: block;
    font-size: var(--text-00);
    color: var(--danger-11);
  }
  .n-error::before {
    content: '⚠';
    font-size: 0.9em;
    margin-inline-end: 0.35em;
  }

  /* ── checkbox, radio, switch ─────────────────────────────────────────────── */

  .n-check, .n-radio {
    appearance: none;
    flex: none;
    inline-size: 1.125rem;
    block-size: 1.125rem;
    margin: 0;
    border: var(--line-1) solid var(--neutral-7);
    background: var(--neutral-1);
    cursor: pointer;
    display: grid;
    place-content: center;
    transition: background var(--dur-0) var(--ease-out), border-color var(--dur-0) var(--ease-out);
  }
  .n-check { border-radius: var(--r-1); }
  .n-radio { border-radius: var(--r-full); }

  .n-check::before {
    content: '';
    inline-size: 0.625rem;
    block-size: 0.625rem;
    background: var(--brand-ink);
    clip-path: polygon(14% 44%, 0 65%, 43% 100%, 100% 16%, 85% 0, 41% 71%);
    scale: 0;
    transition: scale var(--dur-1) var(--ease-spring);
  }
  .n-radio::before {
    content: '';
    inline-size: 0.5rem;
    block-size: 0.5rem;
    border-radius: var(--r-full);
    background: var(--brand-ink);
    scale: 0;
    transition: scale var(--dur-1) var(--ease-spring);
  }
  .n-check:checked, .n-radio:checked { background: var(--brand-9); border-color: var(--brand-9); }
  .n-check:checked::before, .n-radio:checked::before { scale: 1; }

  /* Indeterminate is a real tri-state and most libraries forget it. */
  .n-check:indeterminate { background: var(--brand-9); border-color: var(--brand-9); }
  .n-check:indeterminate::before {
    clip-path: polygon(10% 40%, 90% 40%, 90% 60%, 10% 60%);
    scale: 1;
  }

  .n-switch {
    appearance: none;
    flex: none;
    inline-size: 2.25rem;
    block-size: 1.25rem;
    margin: 0;
    border-radius: var(--r-full);
    background: var(--neutral-6);
    /* THE THIRD TOGGLE THAT DID NOT GET THE BOUNDARY.
     *
     * .n-check and .n-radio both carry --neutral-7 and this had `border: none`, so an
     * unchecked switch was a --neutral-6 pill at 1.5500:1 against the page and 1.3235:1 on
     * a card in dark. Its STATE was never in doubt — checked is --brand-9 at 4.3107:1, a
     * different colour entirely — but a control you cannot find is 1.4.11 all the same, and
     * 3:1 for a control boundary is the reason step 7 exists.
     *
     * box-sizing is border-box globally, so the 1px edge takes the padding box from 36x20
     * to 34x18 and the knob's inset drops from 0.1875rem to 0.125rem to stay centred. The
     * checked translate is still exactly 1rem: 2 + 14 + 16 = 32, which leaves the same 2px
     * at the far end. */
    border: var(--line-1) solid var(--neutral-7);
    cursor: pointer;
    position: relative;
    transition: background var(--dur-1) var(--ease-out);
  }
  .n-switch::before {
    content: '';
    position: absolute;
    /* 0.125rem, not 0.1875rem: the track gained a 1px border above, so these are measured
       from a padding box 2px smaller in each axis. */
    inset-block-start: 0.125rem;
    inset-inline-start: 0.125rem;
    inline-size: 0.875rem;
    block-size: 0.875rem;
    border-radius: var(--r-full);
    background: var(--neutral-1);
    box-shadow: var(--shadow-1);
    transition: translate var(--dur-1) var(--ease-spring);
  }
  .n-switch:checked { background: var(--brand-9); }
  .n-switch:checked::before { translate: 1rem 0; }

  /* Label rows for all three. gap, not margin, so RTL works without overrides. */
  .n-choice {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-1);
    cursor: pointer;
  }
  /* :has() lets the row respond to its own input. Before this, every library needed a
     wrapper class toggled by JS to do the same thing. */
  .n-choice:has(:disabled) { color: var(--neutral-11); cursor: not-allowed; }

  /* ── card ───────────────────────────────────────────────────────────────── */

  .n-card {
    /* --surface, not --neutral-1. In light mode the page is tinted and this is pure
       white; the tint reads as light because the card beside it has none. */
    background: var(--surface);
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-5);
    box-shadow: var(--shadow-1), var(--rim);
  }
  .n-card-pad { padding: var(--space-5); }
  .n-card-float { box-shadow: var(--shadow-2), var(--rim); }
  .n-card-link {
    display: block;
    text-decoration: none;
    color: inherit;
    transition: box-shadow var(--dur-2) var(--ease-out), translate var(--dur-2) var(--ease-out);
  }
  .n-card-link:hover { box-shadow: var(--shadow-3), var(--rim); translate: 0 -2px; }

  /* ── badge, chip ────────────────────────────────────────────────────────── */

  .n-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3125rem;
    padding: 0.1875rem 0.5rem;
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-full);
    background: var(--neutral-3);
    color: var(--neutral-12);
    font-size: var(--text-000);
    font-weight: var(--weight-medium);
    line-height: 1.4;
    white-space: nowrap;
  }
  /* The glyph slot. Required on every status variant — see the header note. */
  .n-badge-glyph {
    flex: none;
    inline-size: 0.875rem;
    block-size: 0.875rem;
    border-radius: var(--r-full);
    display: grid;
    place-items: center;
    font-size: 0.5rem;
    font-weight: var(--weight-bold);
    font-family: var(--font-mono);
  }

  .n-badge-brand { background: var(--brand-3); border-color: var(--brand-6); color: var(--brand-11); }
  .n-badge-brand .n-badge-glyph { background: var(--brand-9); color: var(--brand-ink); }
  .n-badge-ok { background: var(--ok-3); border-color: var(--ok-6); color: var(--ok-11); }
  .n-badge-ok .n-badge-glyph { background: var(--ok-9); color: var(--ok-ink); }
  .n-badge-warn { background: var(--warn-3); border-color: var(--warn-6); color: var(--warn-11); }
  .n-badge-warn .n-badge-glyph { background: var(--warn-9); color: var(--warn-ink); }
  .n-badge-danger { background: var(--danger-3); border-color: var(--danger-6); color: var(--danger-11); }
  .n-badge-danger .n-badge-glyph { background: var(--danger-9); color: var(--danger-ink); }
  .n-badge-info { background: var(--info-3); border-color: var(--info-6); color: var(--info-11); }
  .n-badge-info .n-badge-glyph { background: var(--info-9); color: var(--info-ink); }

  /* ── note / alert ───────────────────────────────────────────────────────── */

  .n-note {
    display: flex;
    gap: var(--space-3);
    align-items: flex-start;
    padding: var(--space-3) var(--space-4);
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-4);
    background: var(--neutral-2);
    color: var(--neutral-12);
    font-size: var(--text-0);
  }
  .n-note-glyph {
    flex: none;
    inline-size: 1.25rem;
    block-size: 1.25rem;
    border-radius: var(--r-full);
    display: grid;
    place-items: center;
    font: var(--weight-bold) 0.75rem/1 var(--font-mono);
    /* Optical alignment with the first line of body text rather than the box top. */
    margin-block-start: 0.0625rem;
  }
  .n-note-title { font-weight: var(--weight-semibold); }
  .n-note-ok { background: var(--ok-2); border-color: var(--ok-6); color: var(--ok-12); }
  .n-note-ok .n-note-glyph { background: var(--ok-9); color: var(--ok-ink); }
  .n-note-warn { background: var(--warn-2); border-color: var(--warn-6); color: var(--warn-12); }
  .n-note-warn .n-note-glyph { background: var(--warn-9); color: var(--warn-ink); }
  .n-note-danger { background: var(--danger-2); border-color: var(--danger-6); color: var(--danger-12); }
  .n-note-danger .n-note-glyph { background: var(--danger-9); color: var(--danger-ink); }
  .n-note-info { background: var(--info-2); border-color: var(--info-6); color: var(--info-12); }
  .n-note-info .n-note-glyph { background: var(--info-9); color: var(--info-ink); }

  /* ── table ──────────────────────────────────────────────────────────────── */

  .n-table { inline-size: 100%; font-size: var(--text-0); }
  .n-table th {
    font: var(--weight-medium) var(--text-000) / 1 var(--font-mono);
    letter-spacing: var(--tracking-micro);
    text-transform: uppercase;
    color: var(--neutral-11);
    padding: 0 var(--space-3) var(--space-2) 0;
    border-block-end: var(--line-1) solid var(--neutral-7);
  }
  .n-table td {
    padding: var(--space-3) var(--space-3) var(--space-3) 0;
    border-block-end: var(--line-1) solid var(--neutral-6);
    color: var(--neutral-11);
  }
  .n-table td:last-child, .n-table th:last-child { padding-inline-end: 0; }
  .n-table-key { color: var(--neutral-12); font-weight: var(--weight-medium); }
  /* Tabular figures, so columns of numbers line up. Nothing else makes a table look
     as unfinished as proportional digits. */
  /* nowrap because a measurement is one token to a reader: "1.4 GB" breaking to "1.4 / GB"
   * at a narrow width destroys the column that tabular-nums exists to line up. */
  .n-table-num {
    text-align: end;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    color: var(--neutral-12);
  }
  /* Step 3, not step 2, and a top rule as the second channel.
   *
   * --neutral-2 is the one hover in this file that failed in LIGHT as well as dark: it
   * measures 1.0550:1 against the page and 1.1186:1 on a card, so the row a pointer was
   * over looked exactly like the rows it was not. Step 3 takes the page case to 1.1300:1
   * light and 1.1700:1 dark. Text is unaffected either way — .n-table td is --neutral-11
   * and that is 4.55:1 on step 3 by construction, since step 11 is solved against step 3.
   *
   * Only a top rule, because td already draws its own --neutral-6 border-block-end; adding
   * a bottom inset here would paint a second line on top of it. The pair reads as a band. */
  .n-table tbody tr:hover td {
    background: var(--neutral-3);
    box-shadow: inset 0 var(--line-1) 0 var(--neutral-6);
  }

  /* Wrapper that scrolls a wide table without breaking the page. tabindex="0" in markup
     makes it keyboard-scrollable, which is a real 2.1.1 requirement people miss. */
  .n-table-scroll { overflow-x: auto; overscroll-behavior-x: contain; }

  /* ── dialog: native, no focus-trap library ──────────────────────────────── */

  .n-dialog {
    /* margin: auto is what centres a modal <dialog>, and it comes from the UA sheet rather
     * than from anything here — so any reset that zeroes margins silently pins the modal to
     * the top-left corner. ANY margin-zeroing reset does it — Tailwind's preflight
     * (`margin: 0` on `*, ::after, ::before, ::backdrop`) and the hand-written
     * `* { margin: 0; padding: 0 }` that half the web starts with. Two separate apps hit it,
     * one Tailwind and one not, which is how the cause got correctly generalised: the first
     * fix here blamed Tailwind and would have left the other one broken.
     *
     * Found by looking at a screenshot rather than by reading either stylesheet, because
     * neither is wrong on its own. */
    margin: auto;
    padding: 0;
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-6);
    background: var(--surface);
    color: var(--neutral-12);
    box-shadow: var(--shadow-3), var(--rim);
    inline-size: min(32rem, calc(100vw - 2rem));
    max-block-size: calc(100dvh - 4rem);
    /* The top layer already handles stacking, so no z-index. */
    overflow: visible;
  }
  .n-dialog::backdrop {
    background: var(--scrim);
    backdrop-filter: blur(2px);
  }
  .n-dialog-head { padding: var(--space-5) var(--space-5) var(--space-3); }
  .n-dialog-title { font-size: var(--text-3); }
  .n-dialog-body { padding-inline: var(--space-5); overflow-y: auto; }
  .n-dialog-foot {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-5) var(--space-5);
  }

  /* Enter and exit animation for an element that is display:none when closed. Needs all
     three parts: allow-discrete to animate display at all, @starting-style for the
     entry values, and overlay so it stays in the top layer while leaving. Miss any one
     and the exit animation silently does not play. */
  .n-dialog, .n-dialog::backdrop {
    transition:
      opacity var(--dur-2) var(--ease-out),
      translate var(--dur-2) var(--ease-out),
      display var(--dur-2) allow-discrete,
      overlay var(--dur-2) allow-discrete;
    opacity: 0;
  }
  .n-dialog { translate: 0 0.5rem; }
  .n-dialog[open], .n-dialog[open]::backdrop { opacity: 1; }
  .n-dialog[open] { translate: 0 0; }
  @starting-style {
    .n-dialog[open], .n-dialog[open]::backdrop { opacity: 0; }
    .n-dialog[open] { translate: 0 0.5rem; }
  }

  /* ── popover, menu, tooltip: native top layer + CSS anchor positioning ──── */

  .n-pop {
    /* [popover] is display:none until shown, so this is the shown state. */
    margin: 0;
    padding: var(--space-2);
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-4);
    background: var(--surface);
    color: var(--neutral-12);
    box-shadow: var(--shadow-2), var(--rim);
    inline-size: max-content;
    max-inline-size: 18rem;

    /* Anchored to its own invoker. A popover opened by popovertarget gets an IMPLICIT
       anchor reference to the button that opened it, so no anchor-name is needed on the
       trigger — this positions itself with no JS and no Floating UI. */
    position-area: block-end span-inline-start;
    position-try-fallbacks: flip-block, flip-inline;
    margin-block-start: var(--space-1);
  }
  .n-pop:popover-open { opacity: 1; }
  .n-pop {
    opacity: 0;
    transition:
      opacity var(--dur-1) var(--ease-out),
      display var(--dur-1) allow-discrete,
      overlay var(--dur-1) allow-discrete;
  }
  @starting-style { .n-pop:popover-open { opacity: 0; } }

  .n-menu { display: flex; flex-direction: column; gap: 1px; min-inline-size: 11rem; }
  .n-menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border: none;
    border-radius: var(--r-2);
    background: transparent;
    color: var(--neutral-12);
    font-size: var(--text-0);
    text-align: start;
    text-decoration: none;
    cursor: pointer;
    inline-size: 100%;
  }
  /* HOVER AND FOCUS ARE SPLIT, and they were not.
   *
   * One rule used to serve both and it ended with `outline: none`, so a focused menu item
   * gave up the global --brand-9 ring (4.9408:1 on a card in dark) and got a --neutral-3
   * tint instead. .n-pop is --surface by definition, so a menu item is ALWAYS on a card,
   * and in dark that tint measures 1.0009:1 — dark --surface is step 3's lightness with
   * the tint removed, so the two differ by chroma and nothing else. Keyboard focus in a
   * menu was therefore invisible in dark mode: WCAG 2.4.7, not a soft preference.
   *
   * So focus keeps the ring the rest of the system uses, and only hover is restyled. The
   * ring is --neutral-6 rather than --neutral-7 because pointer hover is affordance, which
   * 1.4.11 does not govern; it measures 1.3235:1 on a card in dark, which is above the
   * 1.1072:1 ceiling that ANY fill can reach there (see .n-slider-track for the sweep). */
  .n-menu-item:hover {
    background: var(--neutral-3);
    box-shadow: inset 0 0 0 var(--line-1) var(--neutral-6);
  }
  .n-menu-item-danger { color: var(--danger-11); }
  .n-menu-item-danger:hover {
    background: var(--danger-3);
    box-shadow: inset 0 0 0 var(--line-1) var(--danger-6);
  }
  .n-menu-sep { block-size: var(--line-1); background: var(--neutral-6); margin-block: var(--space-1); }

  /* Tooltip. popover="hint" is the right type — it does not close other popovers and it
     does not take focus. Falls back to a plain popover where hint is unsupported. */
  .n-tip {
    margin: 0;
    padding: 0.3125rem var(--space-2);
    border: none;
    border-radius: var(--r-2);
    background: var(--neutral-12);
    color: var(--neutral-1);
    font-size: var(--text-00);
    inline-size: max-content;
    max-inline-size: 16rem;
    position-area: block-start;
    position-try-fallbacks: flip-block;
    margin-block-end: var(--space-1);
  }

  /* ── accordion: <details name> does the state, with no JS ────────────────── */

  .n-accordion { border: var(--line-1) solid var(--neutral-6); border-radius: var(--r-4); overflow: hidden; }
  .n-accordion > details + details { border-block-start: var(--line-1) solid var(--neutral-6); }
  /* display:block, not flex, and the chevron absolutely positioned.
   *
   * Flex with space-between is the obvious way to push a marker to the end, and it is
   * wrong here: flex turns every text node AND every inline element into a separate flex
   * item, so a summary reading "Why is <code>info</code> achromatic?" came out as three
   * items spread across the full width with gaps in the middle of the sentence. Block
   * layout keeps inline content as prose, which is what it is. */
  .n-summary {
    display: block;
    position: relative;
    padding: var(--space-3) var(--space-4);
    padding-inline-end: calc(var(--space-4) + 1.25rem);
    font-size: var(--text-1);
    font-weight: var(--weight-medium);
    cursor: pointer;
    list-style: none;
  }
  .n-summary::-webkit-details-marker { display: none; }
  /* Step 3 for the same reason as the table row: --neutral-2 was 1.0550:1 against the page
     in LIGHT, not only in dark. .n-accordion already draws --neutral-6 rules between its
     items, so the tint is the whole signal here and it has to carry. */
  .n-summary:hover { background: var(--neutral-3); }
  .n-summary::after {
    content: '';
    position: absolute;
    inset-inline-end: var(--space-4);
    inset-block-start: 50%;
    inline-size: 0.5rem;
    block-size: 0.5rem;
    border-inline-end: var(--line-2) solid var(--neutral-11);
    border-block-end: var(--line-2) solid var(--neutral-11);
    /* -70% rather than -50%: rotating a square about its centre leaves the visual
       centre of a chevron below the geometric one. */
    translate: 0 -70%;
    rotate: 45deg;
    transition: rotate var(--dur-1) var(--ease-out);
  }
  details[open] > .n-summary::after { rotate: -135deg; }
  .n-accordion-body { padding: 0 var(--space-4) var(--space-4); font-size: var(--text-0); color: var(--neutral-11); }

  /* ── tabs ───────────────────────────────────────────────────────────────── */

  .n-tabs { display: flex; gap: var(--space-1); border-block-end: var(--line-1) solid var(--neutral-6); }
  .n-tab {
    padding: var(--space-2) var(--space-3);
    border: none;
    border-block-end: var(--line-2) solid transparent;
    background: transparent;
    color: var(--neutral-11);
    font-size: var(--text-0);
    font-weight: var(--weight-medium);
    cursor: pointer;
    margin-block-end: calc(-1 * var(--line-1));
  }
  .n-tab:hover { color: var(--neutral-12); }
  /* aria-selected is the state, so the selector reads it rather than a class. If the
     markup is not accessible the style does not apply — a useful forcing function. */
  .n-tab[aria-selected='true'] { color: var(--brand-11); border-block-end-color: var(--brand-9); }

  /* ── meter / progress ───────────────────────────────────────────────────── */

  /* The BORDER is the fix, and the alternative was worse.
   *
   * The track alone was --neutral-4, which measures 1.08:1 against a card in dark — the same
   * near-invisibility as the old skeleton, and for the same reason: the dark surface sits at
   * step 3's lightness, so anything from the low ramp drawn ON a card disappears. An empty
   * meter had no extent at all; you could not see how much was left.
   *
   * Darkening the track to --neutral-6 fixes that and breaks something else: the fill then
   * fails 3:1 against the track, at 1.98:1 for ok in light. A meter needs BOTH — an outline so
   * the whole control is findable, and a fill/track step so the value is readable — and one
   * lightness cannot serve both.
   *
   * So: step 7 as a border (3.65:1 light, 3.05:1 dark against a card, clearing 1.4.11 for the
   * control) and the quiet track kept for the fill to work against. Found by an app that had to
   * add the border itself.
   *
   * Honest residual: ok and warn fills are 2.56:1 and 2.54:1 against the track in LIGHT mode.
   * Those two hues are light by nature and the track is light, so the pair is thin. Use
   * --ok-10 / --warn-10 (3.0 and 2.98) where the exact extent matters, or put a numeric
   * readout beside the meter — which a dichromat needs anyway, since a continuous bar has
   * nowhere to put the glyph the status components rely on. */
  .n-meter {
    block-size: 0.375rem;
    border: var(--line-1) solid var(--neutral-7);
    border-radius: var(--r-full);
    background: var(--neutral-4);
    overflow: hidden;
  }
  .n-meter-fill {
    block-size: 100%;
    border-radius: inherit;
    background: var(--brand-9);
    transition: inline-size var(--dur-3) var(--ease-out);
  }
  .n-meter-ok .n-meter-fill { background: var(--ok-10); }
  .n-meter-warn .n-meter-fill { background: var(--warn-10); }
  .n-meter-danger .n-meter-fill { background: var(--danger-9); }

  /* ── avatar ─────────────────────────────────────────────────────────────── */

  .n-avatar {
    inline-size: 2rem;
    block-size: 2rem;
    flex: none;
    border-radius: var(--r-full);
    /* The ring is the shape. --brand-3 on a card measures 1.1973:1 in light and 1.0013:1
       in dark, so without it the disc vanishes on a card in dark mode and the initials are
       left floating in the layout with nothing round them. --brand-6 is 1.3235:1 there,
       which is the most a 1px edge can do without reading as a bordered button. */
    border: var(--line-1) solid var(--brand-6);
    background: var(--brand-3);
    color: var(--brand-11);
    display: grid;
    place-items: center;
    font-size: var(--text-00);
    font-weight: var(--weight-semibold);
    overflow: hidden;
    user-select: none;
  }
  .n-avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }
  .n-avatar-sm { inline-size: 1.5rem; block-size: 1.5rem; font-size: var(--text-000); }
  .n-avatar-lg { inline-size: 3rem; block-size: 3rem; font-size: var(--text-1); }

  /* ── skeleton ───────────────────────────────────────────────────────────── */

  /* Steps 5 and 7, not 3 and 4, and the reason is a measured defect.
   *
   * In DARK mode --surface and --neutral-3 have IDENTICAL lightness — 0.2455 both, differing
   * only by 0.007 of chroma, which is 1.0009:1. So a skeleton built from step 3 was
   * completely invisible on a card, and only the step-4 shimmer stop showed, and only while
   * it travelled. That is not a subtle miss: the component whose entire job is to say
   * "content is coming" said nothing on the surface it is most often placed on. It was true
   * in this file's own showcase for several releases.
   *
   * It happens because solveScale deliberately sets the dark surface to step 3's lightness
   * with the tint removed — the card is the untinted version of the component surface. Good
   * for cards, fatal for anything drawn ON one out of step 3.
   *
   * 5 -> 7 measures 1.18:1 against a dark card and 1.38:1 against the dark page, with a
   * 2.59:1 travelling stop. Still quiet on the page, and actually present on a card. */
  .n-skeleton {
    background: linear-gradient(90deg, var(--neutral-5) 25%, var(--neutral-7) 50%, var(--neutral-5) 75%);
    background-size: 200% 100%;
    border-radius: var(--r-2);
    animation: n-shimmer 1.4s ease-in-out infinite;
  }
  @keyframes n-shimmer { to { background-position: -200% 0; } }

  /* ── link ───────────────────────────────────────────────────────────────── */

  .n-link {
    color: var(--brand-11);
    text-decoration-line: underline;
    text-decoration-color: var(--brand-8);
    text-decoration-thickness: from-font;
    text-underline-offset: 0.15em;
  }
  .n-link:hover { text-decoration-color: currentColor; }

  /* ── layout ─────────────────────────────────────────────────────────────── */

  .n-container { inline-size: 100%; max-inline-size: 72rem; margin-inline: auto; padding-inline: var(--space-4); }
  .n-stack { display: flex; flex-direction: column; gap: var(--space-4); }
  .n-cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
  .n-divider { block-size: var(--line-1); background: var(--neutral-6); border: none; margin: 0; }

  /* Prose. The one place headings get sized, because everywhere else the author decides. */
  .n-prose { max-inline-size: var(--measure); }
  .n-prose > * + * { margin-block-start: var(--space-4); }
  .n-prose h2 { font-size: var(--text-4); margin-block-start: var(--space-6); }
  .n-prose h3 { font-size: var(--text-3); margin-block-start: var(--space-5); }
  .n-prose :where(ul, ol) { padding-inline-start: var(--space-5); }
  .n-prose ul { list-style: disc; }
  .n-prose ol { list-style: decimal; }
  .n-prose li + li { margin-block-start: var(--space-2); }
  /* The border is here because <pre> below has one and inline code did not. --neutral-3 on
     a card is 1.0009:1 in dark, so the tint is doing nothing there and the mono face was
     carrying the whole distinction on its own. */
  .n-prose code {
    background: var(--neutral-3);
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-1);
    padding: 0.1em 0.3em;
  }
  .n-prose pre {
    background: var(--neutral-2);
    border: var(--line-1) solid var(--neutral-6);
    border-radius: var(--r-4);
    padding: var(--space-4);
  }
  .n-prose pre code { background: none; padding: 0; }
  .n-prose blockquote {
    border-inline-start: var(--line-2) solid var(--brand-8);
    padding-inline-start: var(--space-4);
    color: var(--neutral-11);
  }

  /* ── kbd, num ───────────────────────────────────────────────────────────── */

  /* A keyboard hint that does not look like inline code.
   *
   * nilam.base.css already gives `code, kbd, samp, pre` the mono face at 0.9375em, which
   * is type and nothing else — so <kbd>I</kbd> and <code>I</code> rendered identically.
   * That is wrong in a way that matters for tool UIs: "press I" and "the value I" are
   * different claims, and nilam's OWN behaviours page has been writing <kbd>→</kbd> beside
   * inline code for five releases with no way to tell them apart. Found by someone
   * building a keyboard-driven editor on it and reaching for a class that was not there.
   *
   * The box, not just a tint: --neutral-3 is 1.0009:1 on a card in dark, so a background
   * alone would have shipped the same non-distinction it was added to fix. --neutral-7 at
   * 3.0469:1 is what makes it read as a key. */
  .n-kbd {
    display: inline-block;
    font-family: var(--font-mono);
    /* Not the 0.9375em from base: a key cap sits inside running text and one step down
       keeps the line from growing round it. */
    font-size: var(--text-00);
    line-height: 1;
    /* min-inline-size so a single character does not produce a key narrower than it is
       tall — "K" and "Esc" should read as the same kind of object. */
    min-inline-size: 1.5em;
    padding: 0.25em 0.4em;
    border: var(--line-1) solid var(--neutral-7);
    /* The bottom edge is the key travel. One border-width, not a blurred shadow, so it
       survives forced-colors and prints. */
    border-block-end-width: var(--line-2);
    border-radius: var(--r-1);
    background: var(--neutral-3);
    color: var(--neutral-12);
    text-align: center;
    /* A shortcut is one token. "Ctrl" breaking after "Ct" is not a key. */
    white-space: nowrap;
  }
  /* A combination is a sequence of keys, so the joiner lives outside them. */
  .n-kbd + .n-kbd { margin-inline-start: var(--space-1); }

  /* Tabular figures on their own, for numbers outside a table.
   *
   * .n-table-num does this and three other things — text-align: end, nowrap, and
   * --neutral-12 — because inside a table all four are right together. Outside one, the
   * alignment is wrong and the colour is a decision the caller should make, so reaching
   * for .n-table-num on a standalone figure imported two properties nobody asked for.
   *
   * Deliberately NO alignment and NO colour. The whole reason this exists rather than
   * `tabular-nums` at each call site is that a house rule — every number that updates gets
   * tabular figures — is only enforceable if it has a name. */
  .n-num { font-variant-numeric: tabular-nums; font-feature-settings: 'tnum' 1; }

  /* ── utilities that are genuinely load-bearing ──────────────────────────── */

  /* The only correct visually-hidden. `display:none` removes it from the a11y tree and
     `text-indent:-9999px` breaks RTL and gets read as a huge empty box. */
  .n-sr-only {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  /* Skip link: hidden until focused, then a real target. 2.4.1. */
  .n-skip {
    position: absolute;
    inset-block-start: var(--space-2);
    inset-inline-start: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-3);
    background: var(--brand-9);
    color: var(--brand-ink);
    text-decoration: none;
    translate: 0 -200%;
  }
  .n-skip:focus-visible { translate: 0 0; }
}
