/* ==================================================================
   NEBULA  —  the design language of lucya.systems
   ------------------------------------------------------------------
   This is the portable sheet: the tokens and the base components that
   every lucya.systems app wears. The gallery (app/static/style.css)
   and the configurator (configurator/app/static/style.css) are the two
   production instances of it; this file is what a THIRD app starts
   from, and what the showcase next to it demonstrates.

   Six rules. Every decision in here is one of them:

     1. Black ground, grey furniture, ONE accent.
        --acc marks STATE and nothing else. Furniture is --chrome or
        --label. Before colouring something purple, ask whether it
        carries state. If it doesn't, it isn't purple.
     2. Square corners, always. --radius: 0. Only genuinely round
        things (a status dot) opt out with their own border-radius.
     3. Depth comes from blur, not from darkness. Panes float on
        --glass / --scrim over a drained wallpaper. A surface that
        needs to read better gets more blur, never a darker fill.
     4. The mono, tracked, uppercase voice marks the CHROME AROUND
        the content. Everything a person actually reads is Space
        Grotesk in sentence case. The display face is the wordmark's
        voice and is not a heading's.
     5. MEASURE IS RATIONED LIKE COLOUR. Every size, space, duration
        and blur in this file comes off a named scale. A raw px value
        in an app sheet is the same mistake as a raw hex.
     6. THE ENVIRONMENT CAN OVERRULE THE LOOK. Blur, translucency,
        motion and hover are capabilities, not guarantees. Each has a
        declared fallback, and every one of them is reached by
        redefining tokens — never by overriding rules.

   The colours, the wordmark and the logo belong to whoever runs the
   app. The language does not: it is the same six rules whatever the
   accent is set to.
   ================================================================== */

/* ------------------------------------------------------------------
   FACES
   Static per-weight instances, never a variable font and never a
   local() entry: instantiating a variable face, and searching the
   installed system fonts once per face, were together close to a
   second of main-thread work on a phone's first layout. Each weight
   is its own file so a page fetches only the weights it draws, and
   the weight RANGES mean a requested weight always lands on exactly
   one file and never on a synthesised bold.
   ------------------------------------------------------------------ */
@font-face{ font-family:'Space Grotesk'; src:url('assets/fonts/SpaceGrotesk-400.woff2') format('woff2'); font-weight:100 450; font-style:normal; font-display:swap; }
@font-face{ font-family:'Space Grotesk'; src:url('assets/fonts/SpaceGrotesk-500.woff2') format('woff2'); font-weight:451 550; font-style:normal; font-display:swap; }
@font-face{ font-family:'Space Grotesk'; src:url('assets/fonts/SpaceGrotesk-600.woff2') format('woff2'); font-weight:551 650; font-style:normal; font-display:swap; }
@font-face{ font-family:'Space Grotesk'; src:url('assets/fonts/SpaceGrotesk-700.woff2') format('woff2'); font-weight:651 900; font-style:normal; font-display:swap; }
@font-face{ font-family:'JetBrains Mono'; src:url('assets/fonts/JetBrainsMono-400.woff2') format('woff2'); font-weight:100 450; font-style:normal; font-display:swap; }
@font-face{ font-family:'JetBrains Mono'; src:url('assets/fonts/JetBrainsMono-500.woff2') format('woff2'); font-weight:451 550; font-style:normal; font-display:swap; }
@font-face{ font-family:'JetBrains Mono'; src:url('assets/fonts/JetBrainsMono-600.woff2') format('woff2'); font-weight:551 650; font-style:normal; font-display:swap; }
@font-face{ font-family:'JetBrains Mono'; src:url('assets/fonts/JetBrainsMono-700.woff2') format('woff2'); font-weight:651 900; font-style:normal; font-display:swap; }
/* The display face: the wordmark's voice, the one big word on a
   landing screen, a 404 numeral. Never a section heading. */
@font-face{ font-family:'Ethnocentric'; src:url('assets/fonts/Ethnocentric-Regular.otf') format('opentype'); font-weight:400; font-style:normal; font-display:swap; }

:root{
  /* ================================================================
     GROUND
     ----------------------------------------------------------------
     Black is the floor, grey is the whole body. NOTHING in this ramp
     carries a hue: the palette this replaced mixed every surface
     towards blue-violet, so a screen read as purple end to end and
     the actual accents had nothing quiet to sit against. Colour
     appears once, on purpose, as --acc.
     ================================================================ */
  --bg:          #000000;
  --bg-2:        #08080a;
  --surface:     #0e0e10;
  --surface-2:   #16161a;
  --line:        rgba(255,255,255,0.09);
  --line-strong: rgba(255,255,255,0.20);

  /* ---- the text ramp ---------------------------------------------
     Measured, not picked. Every value here carries a stated contrast
     floor against --surface (#0e0e10), because that — not #000 — is
     what text in this system actually sits on: panels, cards, fields
     and menus are all one or two steps up from the floor.

       4.5:1  anything a person READS  (body, help, empty states,
              and the label of any control that can be operated)
       3.0:1  furniture that is not read as language (hairlines,
              disabled states, decorative marks)

     The reference is --surface AND the --glass-hi composite, which
     are held within a step of each other on purpose: once panels and
     cards float on glass, the ground under reading text is no longer
     a fixed colour, it is whatever the wallpaper happens to be there.
     --glass-hi is opaque enough to bound that drift. This is the cost
     of a glass reading surface and it is worth paying once, in one
     token, rather than discovering it per app.

     --text-ghost is a GHOST: de-emphasised furniture, a placeholder,
     a disabled label. It is not a colour for text that has to be
     read. Anything a person reads bottoms out at --text-dim.

     --text-ghost used to be #6b6b73, which was 3.65:1 on --surface
     and 3.42:1 on --surface-2 — under the floor everywhere it was
     used, and it was used for help text, placeholders and the
     UNSELECTED segments of a segmented control. It is lifted here. */
  --text:        #f2f2f4;   /* 17.3:1 on surface */
  --text-dim:    #a1a1a8;   /*  7.5:1 */
  --text-ghost:  #7d7d86;   /*  4.7:1 — the floor for read text */
  /* one step down from --text: a heading that stops competing with
     the page title above it and the content below it */
  --text-head:   #cdcdd4;   /* 12.2:1 */
  /* the two ends of the display-title gradient */
  --title-a:     #ffffff;
  --title-b:     #8b8b94;

  /* ================================================================
     THE ONE ACCENT
     ----------------------------------------------------------------
     #5865F2, "Nebula Blue", is the BRAND hex — the value a config
     file supplies and the value the wordmark is drawn in. It is not
     used raw. Like any hand-typed accent it goes through the
     derivation (nebulaAccent() in showcase.js, _accent_shades() in
     the gallery's main.py) and comes out as three faces, each one
     lifted or dropped until it clears 4.5:1 against the text it has
     to carry.

     The derivation used to measure against --bg (#000000) and stop
     the moment it cleared it. But the accent is almost never ON
     #000: it is a link inside a .panel, an active .menu__option, an
     .is-on segment — all of them on --surface or higher. #5865F2
     clears 4.56:1 on #000 and only 4.19:1 on --surface, so the whole
     margin was spent before it was used. The reference surface is
     now --surface, which lifts the brand hex by one step:

       #5865F2  (brand, config value)  ->  #616EF3  (--acc, in use)

     Three faces, one colour under three constraints:
       --acc       small text on a PANEL and a fill under black label
                   text; both readings want luminance, so a colour
                   too dark to read is LIFTED
       --acc-deep  the one face that carries WHITE text
       --acc-soft  the lifted step, hover faces under black text
       --on-acc    what goes ON TOP of an --acc fill. Today this is
                   the same value as --bg; it is not the same IDEA,
                   and the derivation already computes the constraint.
                   Spell the idea, so a light-ground fork stays sane. */
  --acc-src:     #5865f2;   /* the brand hex, for reference only */
  --acc:         #616ef3;   /* 4.7:1 on --surface, 5.0:1 on --bg */
  --acc-rgb:     97,110,243;
  --acc-deep:    #404ee7;   /* 6.1:1 under white text */
  --acc-soft:    #a4abf8;   /* 9.0:1 on --surface */
  --on-acc:      #000000;

  /* ---- the grey that is NOT the accent ---------------------------
     Everything that is merely furniture takes one of these two. This
     split IS the design; painting both groups the accent colour is
     exactly what made the old page read violet end to end. */
  --chrome:      #e8e8ee;   /* hovers, corner brackets, HUD strokes */
  --chrome-rgb:  232,232,238;
  --label:       #b8b8c2;   /* the mono furniture voice: labels,
                               counts, badges, measured values */
  --label-rgb:   184,184,194;

  /* ---- status, deliberately dulled -------------------------------
     Neon amber/red on a black-and-grey page reads as an alarm, not as
     information, and fights the accent. --ok is the exception: it is
     picked off the same swatch as --acc, so it is bright. Each has a
     -deep face for the rare fill that must carry white text — the
     same guarantee --acc-deep gives, so a danger button is not the
     one place in the app with unchecked contrast. */
  --amb:      #d9a24a;  --amb-rgb: 217,162,74;   --amb-deep:  #7a5410;
  --ok:       #58f298;  --ok-rgb:  88,242,152;   --ok-deep:   #11723f;
  --red:      #e0625f;  --red-rgb: 224,98,95;    --red-deep:  #97201d;

  /* ================================================================
     RULE 5 — MEASURE
     ----------------------------------------------------------------
     Space, size, tracking, motion and blur, each on one named scale.
     Before this block existed the sheet carried 23 distinct paddings,
     13 gaps, 13 font sizes and 14 tracking values, every one of them
     hand-typed — so "the same as the gallery" was something you had
     to remember rather than something you could reference, and app
     three could only drift.

     The ship check is a grep, the same way rule 1's is a count:
       grep -oE '(padding|gap|margin|font-size):[^;]*[0-9]+px' app.css
     Nothing back means the app is speaking the language.
     ================================================================ */

  /* ---- space: a 4px rhythm ---------------------------------------
     Eight steps, roughly 1.5x apart, so two adjacent values are
     always visibly different. If a layout wants something between
     two steps, the layout is wrong, not the scale.
       1  hairline gaps, chip padding
       2  tight inner padding
       3  the default gap between related things
       4  control padding, the gap inside a component
       5  panel padding
       6  the gap BETWEEN components
       7  block separation
       8  section separation */
  /* The sub-step. Not a space: a BEZEL — the inset of a segmented
     track, the hairline gutter of a stack. Anything a person would
     call "distance between two things" starts at --s-1. */
  --s-0:  2px;
  --s-1:  4px;
  --s-2:  6px;
  --s-3:  8px;
  --s-4:  12px;
  --s-5:  16px;
  --s-6:  24px;
  --s-7:  32px;
  --s-8:  48px;
  /* Page rhythm. The scale stopped at 48 and the doc layout kept
     reaching past it — a chapter gap, the space under a hero — which
     is how off-scale values get typed even by someone holding to the
     rule. If a scale does not cover the page, it only covers the
     components. */
  --s-9:  64px;
  --s-10: 96px;
  --s-11: 144px; /* editorial page rhythm */

  /* ---- type: two voices, four steps each -------------------------
     The names carry rule 4, so the token you reach for tells you
     which voice you are in: --fs-chrome-* is mono, uppercase and
     tracked; --fs-text-* is Space Grotesk in sentence case. Picking
     the wrong one is then visible in the source, not only on screen.

     The old sheet had ten sizes between 9px and 15px — 10px and
     10.5px are not a hierarchy anyone perceives, they are noise that
     reads as inconsistency and lets two apps diverge invisibly. */
  --fs-chrome-xs: 9.5px;   /* card index, field flag, menu head */
  --fs-chrome-sm: 10.5px;  /* the workhorse: buttons, tags, meta */
  --fs-chrome-md: 12px;    /* field labels, menu rows, search */
  --fs-chrome-lg: 14px;    /* a measured value with its own line */
  --fs-chrome-xl: 20px;    /* a headline number: a stat tile, a count */

  /* Icons are not type, but they are sized like it: one step under the cap
     height of whatever label they lead. `.fa` reads this, and so does any
     BOX built to hold an icon — that is what keeps a glyph and an <img>
     emblem the same size in the same slot. */
  --fs-ico:       .92em;

  --fs-text-sm:   12px;    /* help text, footer, key/value */
  --fs-text-md:   13px;    /* inputs, panel body */
  --fs-text-lg:   15px;    /* base body size, leads */
  --fs-text-xl:   18px;    /* a panel heading that leads a screen */
  --fs-title:     clamp(22px, 2.7vw, 33px);
  /* Opt-in landing-page scale; app headings keep their existing measure. */
  --fs-text-editorial: clamp(32px, 4.5vw, 64px);
  --fs-text-hero: clamp(48px, 7.5vw, 112px);
  --tr-editorial: -.045em;
  /* Brand geometry: fit the house wordmark across a viewport. */
  --fs-brand-floor: calc(6.4vw * var(--display-scale));
  --tr-brand-floor: .025em;
  --fs-chrome-display: clamp(32px, 4.5vw, 64px);
  --tr-chrome-display: .02em;
  --mark-spread: 6px;
  --mark-lift: 4px;
  --mark-detail-opacity: .16;
  --splash-travel: 140svh;
  --splash-initial-width: min(50vw, calc(var(--s-11) * 3));
  --brand-floor-opacity: .18;

  /* ---- tracking, bound to size -----------------------------------
     One formula, not fourteen opinions: tracking FALLS as size
     rises, because the reason to track is optical, and small caps
     need the air that large caps do not. Every --fs-chrome-* has
     exactly one --tr-chrome-* that goes with it, same suffix. */
  --tr-chrome-xs: .24em;
  --tr-chrome-sm: .18em;
  --tr-chrome-md: .14em;
  --tr-chrome-lg: .10em;
  --tr-chrome-xl: .04em;    /* the formula continues: bigger, tighter */
  --tr-otp:       .32em;    /* a one-time code, read back digit by digit */
  --tr-text:      .01em;
  --tr-title:     .03em;
  --tr-head:     -.005em;   /* a sans heading tightens, it never tracks */
  --tr-num:       .06em;    /* mono digits in a value column */
  --tr-display:   .14em;    /* the display face, which inks its own em */

  /* Icons are sized off the type scale but are not type: a glyph at
     the same px as its label reads a size too large, because it has
     no x-height to sit under. Two steps is all any of this needs. */
  --icon-sm:  10px;
  --icon-md:  12px;
  --fs-code:  .88em;   /* code is relative: it sits inside running text */

  /* Two durations that are not transitions: a mark's hover split, and the
     fade a page leaves on. They sit here because the door and the mark are
     part of the language now, and a component may not hand-type a duration
     any more than a padding. */
  --mark-split: .5s;
  --boot-fade:  .45s;

  /* ---- motion ----------------------------------------------------
     Three durations. Anything slower than --dur-3 is an animation
     and belongs behind the fx gate, not in a transition. */
  --dur-1:   .15s;   /* colour, border — a state change */
  --dur-2:   .25s;   /* movement, a pane appearing */
  --dur-3:   .35s;   /* an image settling, a scale */
  --ease:     cubic-bezier(.4, 0, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);

  /* ================================================================
     RULE 3 — DEPTH, AS A LADDER
     ----------------------------------------------------------------
     Rule 3's escape hatch is "a pane that does not read gets MORE
     blur". That was not previously reachable: there was one blur
     value per surface type and no second step to move to. Three
     steps, so the instruction is an actual move.

     Saturation climbs with the blur on purpose. Blur averages the
     backdrop towards grey; pulling saturation back up is what keeps
     a heavy pane looking like glass over a picture instead of like
     a sheet of fog.
     ================================================================ */
  --blur-1: blur(10px) saturate(125%);
  --blur-2: blur(20px) saturate(145%);
  --blur-3: blur(32px) saturate(165%);

  /* ---- the six surfaces ------------------------------------------
     Four of these were tokens already; .nav and .menu used to carry
     hand-typed fills and their own backdrop-filter, which meant the
     documented promise of .fx-lite — "redefine the four tokens and
     every surface retunes, nothing else has to know" — was false for
     the two most persistent panes in any app. There are six
     surfaces. Spelling all six is what makes the promise true.

       --glass     panes over the page: fields, buttons, cards, bars
       --scrim     chips laid ON a picture, where the backdrop is
                   whatever the image happens to be there
       --pane      the app bar: spans the viewport, always present,
                   so it is the one surface that may sit lower
       --overlay   menus and dialogs: they must read over ANY content
                   underneath, so this is the one opaque-ish step

     The -hi variant of each is the lifted step. A surface that is
     not reading takes the -hi step or the next blur — never a
     darker fill. */
  --glass:      rgba(30,30,38,.36);
  --glass-hi:   rgba(22,22,29,.74);
  --scrim:      rgba(4,4,8,.46);
  --scrim-hi:   rgba(6,6,10,.64);
  /* --pane is DEEP BLACK, and it is the one surface with a stated reason not
     to take the top of the blur ladder. Two things put colour into a bar:
     a hue in the fill itself, and saturation in the blur behind it. --pane
     was rgba(6,6,10,.72) on --blur-3 (saturate 165%) and got both -- 6,6,10
     is a blue step, which rule 1 does not allow in the ground ramp anyway,
     and 28% of a re-saturated wallpaper is enough to tint the bar an album's
     colour. The bar spans the viewport on every screen and is the frame the
     whole page is read inside; a frame that changes colour per album is not
     furniture any more (user, 2026-09-07: "header bars sollen tiefschwarz
     bleiben, da andere Farben hier einfach was kaputt machen").

     So: neutral black at .86, over --blur-1. Still glass -- the wallpaper
     moves behind it -- but it reads black at every album and under every
     accent. --pane-scrolled is the deeper step for a bar that has content
     under it; that is a STATE cue, not the legibility fix rule 3 forbids. */
  --pane:          rgba(0,0,0,.86);
  --pane-scrolled: rgba(0,0,0,.94);
  --overlay:       rgba(10,10,14,.82);

  --glass-blur:   var(--blur-2);
  --scrim-blur:   var(--blur-1);
  --pane-blur:    var(--blur-1);
  --overlay-blur: var(--blur-3);

  /* ---- no bevel, on purpose --------------------------------------
     A pane here is a HAIRLINE and a BLUR. It is not lit from the top
     edge and it carries no gloss gradient down its face.

     Those two things — a white inset on the top border and a light
     wash falling off the top of the fill — are what make translucency
     read as Aero: a raised, moulded sheet of glass with a thickness
     to it. That is a different design language, and it fights every
     other rule here. Square corners and a bevel are contradictory
     claims about the same object; a bevel is a soft edge drawn on a
     hard one.

     What this language means by glass is flatness with DEPTH BEHIND
     it: the pane itself is a plane, and what you see through it is
     out of focus. All of the depth is in the blur, none of it is in
     the surface. So there is no --sheen and no --edge token to
     reach for, and a component that wants to look more present asks
     for the next blur step or the -hi fill. */

  /* ---- elevation --------------------------------------------------
     Two shadows and one glow, instead of nine hand-typed one-offs
     (the old sheet spent three different values on what was meant to
     be the same accent glow). Shadows are for things that genuinely
     float ABOVE the page — a menu, a dialog. A card does not float. */
  --shadow-1: 0 2px 10px rgba(0,0,0,.35);
  --shadow-2: 0 18px 44px rgba(0,0,0,.55);
  --glow-acc: 0 0 20px -6px rgba(var(--acc-rgb),.70);
  --ring-acc: 0 0 0 1px rgba(var(--acc-rgb),.22);

  /* ---- shape and layout ------------------------------------------ */
  /* The smallest target that is reliably hit with a thumb. It used to live
     inside the narrow media query, where nothing above 760px could reach it
     — and a submit button on a door is 44px on a desktop too. */
  /* MIST. Nebula's identity tint: the silver of the title gradient
     (--title-a -> --title-b) leaning a little toward --acc, so it reads as
     part of the chrome, not as a second palette. It marks what KIND of
     thing a heading, row or place is about -- on the glyph and on the marks
     of a chart -- never text, a surface, a border or a control. The four
     kind tokens below all point at it: one family, so nothing competes
     with the accent. */
  --mist:   #B4BCDF;   /* the one identity tint: the title silver leaning toward --acc */
  --mist-1: #D8DDF3; --mist-2: #ADB6D8; --mist-3: #858FBE; --mist-4: #5F6AA3;
  --glyph-album:   var(--mist);   /* albums, folders, photos, the site */
  --glyph-tag:   var(--mist);   /* tags */
  --glyph-time:    var(--mist);   /* time: dates, history, activity */
  --glyph-machine: var(--mist);   /* the machine and the camera */
  /* CHART RAMPS -- mist in four steps, light to dark, for a chart whose
     parts are RANKED (a ring's slices, largest first); past four, one grey
     other. Validated on the ground as ordinal: monotone L, clear gaps. */
  --ramp-album-1:   var(--mist-1); --ramp-album-2:   var(--mist-2); --ramp-album-3:   var(--mist-3); --ramp-album-4:   var(--mist-4);
  --ramp-tag-1:   var(--mist-1); --ramp-tag-2:   var(--mist-2); --ramp-tag-3:   var(--mist-3); --ramp-tag-4:   var(--mist-4);
  --ramp-time-1:    var(--mist-1); --ramp-time-2:    var(--mist-2); --ramp-time-3:    var(--mist-3); --ramp-time-4:    var(--mist-4);
  --ramp-machine-1: var(--mist-1); --ramp-machine-2: var(--mist-2); --ramp-machine-3: var(--mist-3); --ramp-machine-4: var(--mist-4);
  --ramp-other:    #4A4A55;
  --tap:        44px;
  --radius:     0;         /* square, everywhere */
  --hair:       1px;       /* the one border width in the system */
  --nav-h:      60px;  --door-w:     420px;     /* the sign-in card: one readable column */

  /* The gutter every band holds to. A clamp, not a constant: 20px is
     right at 1440 and wasteful at 380, where it is the difference
     between a card grid of one column and of two. */
  --page-pad:   clamp(14px, 1.4vw + 9px, 24px);
  --page-max:   2000px;
  /* The one breakpoint the language owns. Below it an app is a
     single column, the bar sheds its middle, and touch targets grow.
     Apps may add their own; they may not disagree about this one. */
  --bp-narrow:  760px;

  /* The mono face, spelled once. The tail past the webfont matters:
     while JetBrains Mono is loading — or silently failing to — this is
     what is on screen, and `monospace` alone resolves to a face some
     6% narrower than `ui-monospace` does. */
  --mono:        "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
  --sans:        "Space Grotesk", system-ui, sans-serif;
  --display-font:"Ethnocentric", "Space Grotesk", sans-serif;
  /* display faces disagree wildly about how much of the em they ink,
     so whoever brings the face also says how to size it */
  --display-scale: 1;

  /* ---- the backdrop ----------------------------------------------
     How the wallpaper is treated before anything is laid over it. A
     token because an app's config can retune it per page. */
  --wallpaper-filter: grayscale(.92) brightness(.72) contrast(1.04);
  /* The one COLOURED thing over the wallpaper. It belongs to the
     tint, not to the dimming: turning the tint off must set this
     transparent too, or a backdrop asked for in full colour still has
     an accent wash sitting on it. */
  --wallpaper-bloom: rgba(var(--acc-rgb),0.07);
  /* The dim laid over the picture, as a pair, because the two ends are
     a real decision and the apps disagree about it. The ramp below is
     for a PHOTOGRAPH: it darkens towards the footer of a scrolling
     page, where the picture is still at full strength. Artwork that
     already falls off downward wants both ends flat, or the ramp lands
     on the art's own falloff and the bottom half goes black. */
  --wallpaper-shade-top: rgba(0,0,0,0.62);
  --wallpaper-shade-bot: rgba(0,0,0,0.82);
  --wallpaper-grid: rgba(255,255,255,.03);
}

/* ---- THE PALETTES (gallery.cfg `palette`, on <html data-palette>) ------
   Three ways to tint the kind-icons and the charts, all from the title
   silver. mist is the :root default above; these only redefine tokens
   (rule 6), so every rule that reads --glyph-* / --ramp-* follows. */
:root[data-palette="silver"] {
  --mist:   #BDBDC5;
  --mist-1: #E7E7F0; --mist-2: #BDBDC5; --mist-3: #94949D; --mist-4: #6E6E76;
}
:root[data-palette="stardust"] {
  --glyph-album: #BDAFD9; --glyph-tag: #D0A2B6; --glyph-time: #98BCDC; --glyph-machine: #BCBDC7;
  --ramp-album-1:   #E1D9F3; --ramp-album-2:   #BDAFD9; --ramp-album-3:   #988AB2; --ramp-album-4:   #74678D;
  --ramp-tag-1:     #F2D5E1; --ramp-tag-2:     #D6A8BD; --ramp-tag-3:     #AF8397; --ramp-tag-4:     #8A6073;
  --ramp-time-1:    #CBE1F5; --ramp-time-2:    #98BCDC; --ramp-time-3:    #7397B5; --ramp-time-4:    #517390;
  --ramp-machine-1: #DDDDE4; --ramp-machine-2: #B6B6C1; --ramp-machine-3: #91919B; --ramp-machine-4: #6D6E77;
}

/* ==================================================================
   RULE 6 — THE ENVIRONMENT GATES
   ------------------------------------------------------------------
   Blur, translucency, motion and hover are capabilities, not
   guarantees. Every gate below reaches its fallback by REDEFINING
   TOKENS, never by overriding component rules — which is the only
   reason a component can be written once and still be correct on a
   four-year-old phone, in Windows high-contrast mode, and for a
   visitor who has asked the OS for less.
   ================================================================== */

/* --- lite: JS-detected weak hardware / data saver / reduced motion.
   No blur, so the panes lose the one thing making them readable —
   pay for it in opacity instead. Nothing else changes, because
   there is nothing else: a pane is a fill and a hairline, and the
   blur was the only part the hardware could not afford. */
.fx-lite{
  --glass:      rgba(22,22,27,.86);
  --glass-hi:   rgba(36,36,44,.94);
  --scrim:      rgba(0,0,0,.78);
  --scrim-hi:   rgba(6,6,10,.88);
  /* --pane and --overlay go FULLY opaque here, and the two glass
     steps do not. The difference is what is behind them: the
     wallpaper is behind a card, but PAGE CONTENT scrolls behind the
     app bar and under a menu. Translucency without a blur to average
     it is not glass, it is a smear — at .96 the hero title still
     ghosted through the bar, because 4% of white over near-black is
     visible. A surface with live content behind it has no business
     being translucent once the blur is gone. */
  --pane:          #000000;
  --pane-scrolled: #000000;
  --overlay:       #0a0a0e;
  --blur-1: none; --blur-2: none; --blur-3: none;
}

/* --- the OS-level version of the same request. .fx-lite is set by
   script and can therefore never run before first paint; this one
   does, so a visitor who has asked for less transparency never sees
   a frame of the glass build. Same tokens, so nothing else changes. */
@media (prefers-reduced-transparency: reduce){
  :root{
    --glass:      rgba(22,22,27,.92);
    --glass-hi:   rgba(36,36,44,.96);
    --scrim:      rgba(0,0,0,.88);
    --scrim-hi:   rgba(6,6,10,.94);
    --pane:          #000000;
    --pane-scrolled: #000000;
    --overlay:       #0a0a0e;
    --blur-1: none; --blur-2: none; --blur-3: none;
  }
}

/* --- more contrast asked for: firm up every hairline and drop the
   ghost step onto the read floor. The look does not change; the
   edges simply stop being suggestions. */
@media (prefers-contrast: more){
  :root{
    --line:        rgba(255,255,255,0.22);
    --line-strong: rgba(255,255,255,0.42);
    --text-ghost:  #9a9aa4;
    --text-dim:    #c2c2c9;
  }
}

/* --- forced colours (Windows high contrast). The entire depth model
   here is translucency plus hairlines, and both are discarded by the
   OS compositor — a page that says nothing about this renders as
   unseparated text on one flat ground. So: give every surface a real
   border in a SYSTEM colour, and let the OS own the palette. This is
   the one place the language hands over its colours, and it should:
   the visitor asked the OS for them, not us. */
@media (forced-colors: active){
  :root{
    --line: CanvasText; --line-strong: CanvasText;
    --glow-acc: none; --shadow-1: none; --shadow-2: none;
  }
  .nav, .menu, .card, .panel, .stack, .field, .btn, .seg, .tag,
  .hud, .nav__search{ border:var(--hair) solid CanvasText; }
  .switch, .dlg, .toast, .avatar, .set__ico, .meter, .login__card,
  .login__why, .rail__nav, .tbl td, .tbl th{ border:var(--hair) solid CanvasText; }
  /* the split is two fixed hexes blended to screen, and neither survives a
     palette the OS owns — a half-blended copy is worse than none */
  .mark__ghost{ display:none; }
  .card::before, .card::after, .hud__corner{ forced-color-adjust:none; }
  .btn--primary, .tag.is-active, .seg__opt.is-on, .menu__option.is-active{
    forced-color-adjust:none;
    background:Highlight; color:HighlightText; border-color:Highlight;
  }
  .switch[aria-checked="true"], .meter__fill{ forced-color-adjust:none; background:Highlight; }
  .rail__nav .menu__option[aria-current="page"]{
    forced-color-adjust:none; background:Highlight; color:HighlightText;
  }
}

/* ==================================================================
   BASE
   ================================================================== */
*{ box-sizing:border-box; }
html,body{ margin:0; padding:0; }
html{ scroll-behavior:smooth; }

body{
  color:var(--text);
  background:var(--bg);
  font-family:var(--sans);
  font-size:var(--fs-text-lg);
  line-height:1.55;
  letter-spacing:var(--tr-text);
  min-height:100vh;
  /* clip, not hidden: overflow-x:hidden forces overflow-y to compute
     to auto, which turns <body> into a scroll container — and iOS
     Safari then scrolls the fixed backdrop with the page. */
  overflow-x:hidden;
  overflow-x:clip;
  isolation:isolate;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

/* `hidden` is a statement about the DOM and it has to WIN. Every component
   below sets its own display — .btn is inline-flex, .field is flex, .seg is
   inline-flex — and each of them beats the attribute on specificity alone,
   which leaves an element the markup calls hidden on screen, with its
   controls still in the tab order and its fields still in the form. This is
   the one place the sheet uses !important, and it is what the attribute
   already means. */
[hidden]{ display:none !important; }

a{ color:inherit; text-decoration:none; }
a:hover{ color:var(--acc); }
img{ user-select:none; -webkit-user-drag:none; max-width:100%; }

/* One focus ring for the whole system. Two layers on purpose: the
   accent outline states WHERE focus is, the dark ring under it keeps
   that outline legible when the focused thing sits on a bright card
   image or on an accent fill. */
:focus-visible{
  outline:2px solid var(--acc);
  outline-offset:2px;
  box-shadow:0 0 0 4px rgba(0,0,0,.55);
}
/* Never take the ring away without giving one back. */
:focus:not(:focus-visible){ outline:none; }

code{
  font-family:var(--mono);
  font-size:var(--fs-code);
  background:rgba(255,255,255,.05);
  border:var(--hair) solid var(--line);
  padding:var(--hair) var(--s-2);
  color:var(--text);
}

.mono { font-family:var(--mono); letter-spacing:0; }
.dim  { color:var(--text-dim); }
.ghost{ color:var(--text-ghost); }
.small{ font-size:var(--fs-text-sm); }

/* The chrome voice, as one class — so an app that needs the voice on
   an element the vocabulary does not cover still gets it right
   instead of hand-typing a fourth set of numbers. */
.chrome-voice{
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm);
  letter-spacing:var(--tr-chrome-sm);
  text-transform:uppercase;
  color:var(--label);
}

/* Numbers that are compared down a column must not shift. Anything
   measured — a count, a size, a timestamp — takes this. */
.num, .tabular{ font-variant-numeric:tabular-nums; }

/* ------------------------------------------------------------------
   THE SITE BACKDROP
   ONE real fixed element, not body::before/::after — iOS Safari
   mispositions fixed pseudo-elements of a flex <body> during scroll.
   The picture is atmosphere, not content: drained to near-greyscale
   and pulled down so the only saturated things on a screen are the
   content and the accent. Not fully grey (.92) — a trace of the
   original hue keeps it from looking like a broken image.
   ------------------------------------------------------------------ */
.site-bg{
  position:fixed; top:0; left:0;
  width:100%; height:100vh; height:100lvh;
  z-index:-1; pointer-events:none;
  background:var(--bg);
}
/* `.site-bg picture` as well as the img: a <picture> is an inline box
   that would otherwise sit at its own size in the corner — the element
   that has to fill the backdrop is the one the browser lays out, and
   with an art-directed source that is the wrapper. */
.site-bg picture{ position:absolute; inset:0; display:block; }
.site-bg img{
  position:absolute; inset:0;
  width:100%; height:100%;
  object-fit:cover;
  filter:var(--wallpaper-filter);
}
/* grid overlay + one accent bloom + darkening */
.site-bg::after{
  content:"";
  position:absolute; inset:0;
  background-image:
    linear-gradient(to right, var(--wallpaper-grid) 1px, transparent 1px),
    linear-gradient(to bottom, var(--wallpaper-grid) 1px, transparent 1px),
    radial-gradient(ellipse at 78% 68%, var(--wallpaper-bloom) 0%, transparent 52%),
    linear-gradient(var(--wallpaper-shade-top), var(--wallpaper-shade-bot));
  background-size:32px 32px, 32px 32px, auto, auto;
  background-repeat:repeat, repeat, no-repeat, no-repeat;
}

/* ==================================================================
   THE GLASS TREATMENT
   ------------------------------------------------------------------
   Rule 3, as one reusable move, so an element the vocabulary does
   not cover still floats the same way the vocabulary does. Every
   pane in this sheet is this: a translucent fill, a blur behind it,
   and a hairline border. Nothing else — no bevel, no gloss. Take
   .u-glass on anything new.

   The two -webkit- lines are not optional: Safari still ships the
   prefixed property only, and without them a pane there is a flat
   translucent rectangle over an unblurred backdrop, which is the one
   result rule 3 exists to prevent.
   ================================================================== */
.u-glass{
  background-color:var(--glass);
  border:var(--hair) solid var(--line);
  border-radius:var(--radius);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
}
.u-scrim{
  background-color:var(--scrim);
  border:var(--hair) solid var(--line);
  border-radius:var(--radius);
  backdrop-filter:var(--scrim-blur);
  -webkit-backdrop-filter:var(--scrim-blur);
}

/* ==================================================================
   NAV  —  the bar every app carries
   ------------------------------------------------------------------
   On --pane, not on a hand-typed fill: the bar is a surface like any
   other, and it is the one every app shows on every screen, so it is
   the first thing that must obey .fx-lite and reduced-transparency.
   ================================================================== */
.nav{
  position:sticky; top:0; z-index:100;
  height:var(--nav-h);
  background-color:var(--pane);
  backdrop-filter:var(--pane-blur);
  -webkit-backdrop-filter:var(--pane-blur);
  border-bottom:var(--hair) solid var(--line);
  transition:background-color var(--dur-2) var(--ease);
}
/* the accent hairline riding on the bottom border — the one place the
   accent is decoration, and it is one pixel of it */
.nav::after{
  content:"";
  position:absolute; left:0; right:0; bottom:-1px; height:1px;
  pointer-events:none;
  background:linear-gradient(90deg,
    transparent 2%, rgba(var(--acc-rgb),.5) 30%,
    rgba(var(--acc-rgb),.45) 50%, rgba(var(--acc-rgb),.5) 70%, transparent 98%);
  opacity:.35;
  transition:opacity var(--dur-2) var(--ease);
}
/* Once there is content under it the bar goes to --pane-scrolled and its
   accent hairline brightens. Deeper, not lighter: black is the point of
   --pane, and this is a state cue rather than the "it will not read" fix
   rule 3 answers with more blur. */
.nav--scrolled{ background-color:var(--pane-scrolled); }
.nav--scrolled::after{ opacity:.85; }
.nav__inner{
  max-width:var(--page-max);
  margin:0 auto; height:100%;
  padding:0 var(--page-pad);
  display:flex; align-items:center; gap:var(--s-6);
  min-width:0;
}
.nav__brand{ display:flex; align-items:center; gap:var(--s-4); color:var(--text); white-space:nowrap; min-width:0; }
.nav__brand:hover{ color:var(--text); }
.nav__brand img{
  height:28px; width:auto; display:block;
  filter:drop-shadow(0 0 12px rgba(var(--acc-rgb),.35));
}
.nav__brand .brand-text{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
.nav__brand .brand-name{
  font-family:var(--display-font);
  font-size:calc(15px * var(--display-scale));
  letter-spacing:var(--tr-display);
  text-transform:uppercase;
  color:var(--text);
}
/* The app name under the wordmark: identical in every app, so two of
   them read as two faces of one system. The colour lifts the flat
   accent with a touch of the soft face — --acc alone goes a little
   muddy against black at this size. */
.nav__brand .brand-sub{
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs);
  letter-spacing:var(--tr-chrome-xs);
  text-transform:uppercase;
  color:color-mix(in srgb, var(--acc-soft) 30%, var(--acc));
}

.nav__search{
  flex:1; min-width:0; max-width:560px;
  margin-inline:auto;
  display:flex; align-items:center; gap:var(--s-3);
  background-color:var(--glass);
  border:var(--hair) solid var(--line);
  border-radius:var(--radius);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
  padding:0 var(--s-4);
  transition:border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.nav__search:focus-within{
  border-color:var(--acc);
  background-color:rgba(var(--acc-rgb),.06);
  box-shadow:var(--ring-acc), var(--glow-acc);
}
.nav__search i{ color:var(--text-ghost); font-size:var(--icon-md); }
/* [type="search"] in the selector, not a bare `input`: the generic field
   block further down this file matches on the same attribute and would
   otherwise win on source order alone — same specificity, later in the
   sheet — and drop a second bordered, filled, sans-serif box INSIDE the
   bar. The wrapper owns the surface here; the input owns only the text. */
.nav__search input[type="search"]{
  flex:1; min-width:0; width:auto;
  appearance:none; -webkit-appearance:none;
  background:none; background-color:transparent;
  border:0; border-radius:0; box-shadow:none; outline:0;
  color:var(--text);
  font-family:var(--mono);
  font-size:var(--fs-chrome-md);
  letter-spacing:var(--tr-chrome-lg);
  padding:var(--s-3) 0;
}
/* the ring belongs to the bar, via :focus-within — never to the field
   inside it, or the two of them draw the same state twice */
.nav__search input[type="search"]:focus,
.nav__search input[type="search"]:focus-visible{
  background-color:transparent; border:0; box-shadow:none; outline:none;
}
.nav__search input[type="search"]::-webkit-search-decoration,
.nav__search input[type="search"]::-webkit-search-cancel-button,
.nav__search input[type="search"]::-webkit-search-results-button{
  -webkit-appearance:none; appearance:none;
}
.nav__search input[type="search"]::placeholder{
  color:var(--text-ghost);
  letter-spacing:var(--tr-chrome-md); text-transform:uppercase; font-size:var(--fs-chrome-sm);
}

/* ==================================================================
   PAGE HEADER  —  a meta line, then a heading
   ------------------------------------------------------------------
   Not a console readout. This used to be a segmented FILE / CLASS /
   RDY bar, an accent index number, the title, and a bilingual
   ornament: four bands of chrome saying what one heading says.
   Hierarchy is TYPOGRAPHIC. Before adding a bordered, filled strip,
   count how many the page already stacks.
   ================================================================== */
.section__doc{
  display:flex; flex-wrap:wrap; align-items:baseline;
  gap:var(--s-2) var(--s-5);
  margin:0 0 var(--s-5);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-sm); text-transform:uppercase;
  color:var(--text-dim);
}
.section__doc > span{ display:inline-flex; align-items:baseline; gap:var(--s-2); min-width:0; }
.section__doc b{ color:var(--text); font-weight:400; letter-spacing:var(--tr-chrome-md); }
/* qualifiers are marks on the line, not stamps: a hairline outline,
   no dashed "sticker" border and no shouting */
.section__doc-mark{
  color:var(--label);
  border:var(--hair) solid rgba(var(--label-rgb),.35);
  padding:var(--s-0) var(--s-3);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-xs);
}
.section__doc-mark.is-ok{ color:var(--ok); border-color:rgba(var(--ok-rgb),.4); }

.section__head{ margin-bottom:var(--s-6); }
.section__slug{ display:flex; align-items:baseline; gap:var(--s-4); flex-wrap:wrap; min-width:0; margin:0; }
.section__slug .name{
  font-family:var(--sans);
  font-size:var(--fs-title);
  font-weight:700;
  letter-spacing:var(--tr-title);
  line-height:1.06;
  word-break:break-word;
  /* Weight instead of colour: white at the cap line falling to
     mid-grey at the baseline. This used to end in the accent, which
     put a violet tint on the largest text of every page. A grey fall
     does the same job without spending the accent on something that
     carries no state. */
  color:var(--title-a);
  background:linear-gradient(175deg, var(--title-a) 18%, var(--title-b) 96%);
  -webkit-background-clip:text; background-clip:text;
  -webkit-text-fill-color:transparent;
  /* em-relative on purpose, and the one value in this sheet that is
     not off the space scale: it is a descender guard for the gradient
     text-clip, so it has to track the font size rather than the
     layout rhythm. */
  padding-bottom:.04em;
}
.section__slug .name::selection{ -webkit-text-fill-color:var(--text); }

/* Editorial headings stay in the reading voice, even at poster scale. */
.editorial-title{
  margin:0;
  font-family:var(--sans);
  font-size:var(--fs-text-editorial);
  font-weight:500;
  letter-spacing:var(--tr-editorial);
  line-height:1.04;
  color:var(--text);
  overflow-wrap:break-word;
}
.editorial-title--hero{ font-size:var(--fs-text-hero); }

/* The eyebrow over a block of content — a labelled rule, never a
   filled bar. One --acc glyph in front of the label is the whole
   licence the accent has here. */
.eyebrow{
  display:flex; align-items:baseline; gap:var(--s-3);
  margin:0 0 var(--s-4); padding-bottom:var(--s-3);
  border-bottom:var(--hair) solid var(--line);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); font-weight:400; letter-spacing:var(--tr-chrome-xs);
  text-transform:uppercase;
  color:var(--label);
}
.eyebrow > i{ color:var(--acc); font-size:var(--icon-sm); }
.eyebrow__n{ margin-left:auto; font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-sm); color:var(--text-dim); }

/* ==================================================================
   BUTTONS
   ------------------------------------------------------------------
   Mono caps on a glass pane, and a hover in --chrome rather than in
   the accent: a button that merely opens a picker carries no state.
   Exactly ONE button in a row may be --primary — the one that
   commits.

   --primary's LABEL is --text, not --acc. Accent-on-accent-tint was
   the worst contrast in the old sheet (4.14:1 on the tint it sits
   on) and it was on the most important control in every row. The
   general rule it comes from is worth remembering:

     THE ACCENT NEVER SITS ON ITS OWN TINT.
     On an accent fill or tint, the label is --on-acc or --text.
     The accent is carried by the fill and the border instead.

   The button reads MORE primary this way, not less: white on a
   violet-lit face is the strongest thing in the row.
   ================================================================== */
.btn{
  appearance:none;
  background-color:var(--glass);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
  border:var(--hair) solid var(--line);
  border-radius:var(--radius);
  color:var(--text-dim);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-sm); text-transform:uppercase;
  padding:var(--s-2) var(--s-4);
  cursor:pointer; white-space:nowrap;
  display:inline-flex; align-items:center; justify-content:center; gap:var(--s-3);
  transition:color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease),
             background-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.btn:hover:not(:disabled){
  color:var(--chrome);
  border-color:var(--chrome);
  background-color:rgba(var(--chrome-rgb),.08);
}
.btn:active:not(:disabled){ background-color:rgba(var(--chrome-rgb),.14); }
.btn:disabled{ opacity:.4; cursor:not-allowed; }
.btn--primary{
  background-color:rgba(var(--acc-rgb),.16);
  border-color:rgba(var(--acc-rgb),.60);
  color:var(--text);
}
.btn--primary:hover:not(:disabled){
  background-color:rgba(var(--acc-rgb),.28);
  border-color:var(--acc);
  color:#fff;
  box-shadow:var(--glow-acc);
}
.btn--ghost{ background-color:transparent; background-image:none; border-color:transparent; color:var(--text-ghost); box-shadow:none; backdrop-filter:none; -webkit-backdrop-filter:none; }
.btn--ghost:hover:not(:disabled){ background-color:rgba(255,255,255,.06); color:var(--text); border-color:transparent; }
.btn--danger:hover:not(:disabled){
  border-color:var(--red); color:var(--red); background-color:rgba(var(--red-rgb),.10);
}
/* the committing destructive button: a real fill, so it is not one
   hover away from looking like every other outline in the row */
.btn--danger.btn--primary{
  background-color:var(--red-deep); border-color:var(--red-deep); color:#fff;
}
.btn--danger.btn--primary:hover:not(:disabled){
  background-color:var(--red); border-color:var(--red); color:var(--on-acc);
}
.btn--sm{ padding:var(--s-1) var(--s-3); font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-md); }
.btn--icon{ padding:var(--s-1) var(--s-3); font-size:var(--fs-chrome-md); letter-spacing:0; }
.row{ display:flex; gap:var(--s-3); align-items:center; flex-wrap:wrap; }

/* ==================================================================
   SEGMENTED CONTROL
   ------------------------------------------------------------------
   ONE idiom for "pick one of these", wherever it appears: tabs, a
   language selector, a three-state toggle, a view switcher. The
   selected segment is the only thing in it wearing the accent,
   because it is the only thing carrying state — as a FILL and a
   ring, with a --text label, per the rule above.
   ================================================================== */
.seg{
  display:inline-flex; align-items:center; gap:var(--hair);
  padding:var(--s-0);
  background-color:var(--glass);
  border:var(--hair) solid var(--line);
  border-radius:var(--radius);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
  width:fit-content; max-width:100%;
  flex-wrap:wrap;
}
.seg__opt{
  appearance:none; background:none; border:0;
  border-radius:var(--radius);
  color:var(--text-dim);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-md); text-transform:uppercase;
  padding:var(--s-2) var(--s-4);
  cursor:pointer; white-space:nowrap;
  transition:color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
.seg__opt:hover:not(:disabled):not(.is-on){ color:var(--text); background-color:rgba(255,255,255,.06); }
.seg__opt.is-on{
  color:var(--text);
  background-color:rgba(var(--acc-rgb),.20);
  box-shadow:inset 0 0 0 1px rgba(var(--acc-rgb),.55);
}
.seg__opt:disabled{ cursor:not-allowed; opacity:.45; }

/* ==================================================================
   TAG  —  a filter chip. Active is state, so active is the accent,
   and it is the accent as a FILL under --on-acc text (which is what
   --acc is derived to survive).
   ------------------------------------------------------------------
   A tag is a CONTROL. Ship it as <button type="button" class="tag">,
   never a <span>: a chip that cannot be focused or activated from a
   keyboard is a filter half the visitors cannot use, and the styling
   below assumes the button reset. Use `aria-pressed` for is-active.
   ================================================================== */
.tag{
  appearance:none;
  display:inline-flex; align-items:center; gap:var(--s-2);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-sm); text-transform:uppercase;
  color:var(--text-dim);
  padding:var(--s-1) var(--s-3);
  border:var(--hair) solid var(--line);
  border-radius:var(--radius);
  background-color:var(--glass);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
  cursor:pointer;
  transition:border-color var(--dur-1) var(--ease), color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}
.tag:hover{ color:var(--text); border-color:var(--line-strong); background-color:var(--glass-hi); }
.tag.is-active,
.tag[aria-pressed="true"],
.tag.is-active:hover,
.tag[aria-pressed="true"]:hover{
  color:var(--on-acc); background-color:var(--acc); border-color:var(--acc);
}
.tag-bar{ display:flex; flex-wrap:wrap; gap:var(--s-2); }
/* A tag that is a LABEL, not a control — a token name in prose, a
   read-only qualifier on a record. It must not look clickable and
   must not be a <button>: a focus stop that does nothing is worse
   than no focus stop. Ship it as <span class="tag tag--static">. */
.tag--static{ cursor:default; pointer-events:none; color:var(--label); }
.tag--static:hover{ color:var(--label); border-color:var(--line); background-color:var(--glass); }

/* ==================================================================
   FIELDS AND INPUTS
   ------------------------------------------------------------------
   The label is furniture (mono, tracked, --label); the help text is
   something a person reads (sans, sentence case). The left edge is
   the state channel: accent when set, grey when unset, amber when
   staged but not committed.
   ================================================================== */
.field{
  display:flex; flex-direction:column; gap:var(--s-3);
  padding:var(--s-4);
  border:var(--hair) solid var(--line);
  border-left:2px solid var(--acc-soft);
  border-radius:var(--radius);
  background:var(--surface);
  min-width:0;
}
.field.is-unset{ background:transparent; border-left-color:var(--line-strong); }
.field.is-edited{
  border-left-color:var(--amb);
  background:linear-gradient(180deg, rgba(var(--amb-rgb),.06), transparent 60%), var(--surface);
}
.field__top{ display:flex; align-items:center; gap:var(--s-3); }
.field__label{
  flex:1 1 auto;
  font-family:var(--mono);
  font-size:var(--fs-chrome-md); font-weight:400; letter-spacing:var(--tr-chrome-md); text-transform:uppercase;
  color:var(--label);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.field.is-unset .field__label{ color:var(--text-ghost); }
.field__flag{
  flex:0 0 auto;
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-md); text-transform:uppercase;
  color:var(--amb);
}
.field__help{ color:var(--text-ghost); font-size:var(--fs-text-sm); line-height:1.45; }
/* an error is read, so it is sans — and it is announced, so it is
   not colour alone: the glyph carries it for anyone who cannot see
   the red. Never signal state with hue by itself. */
.field__error{
  display:flex; align-items:flex-start; gap:var(--s-2);
  color:var(--red); font-size:var(--fs-text-sm); line-height:1.45;
}
.field.is-error{ border-left-color:var(--red); }

input[type="text"], input[type="search"], input[type="email"], input[type="url"],
input[type="password"], input[type="number"], select, textarea{
  background:var(--surface);
  border:var(--hair) solid var(--line-strong);
  border-radius:var(--radius);
  color:var(--text);
  font-family:var(--sans);
  font-size:var(--fs-text-md);
  padding:var(--s-3) var(--s-4);
  width:100%;
  transition:border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease), box-shadow var(--dur-1) var(--ease);
}
input::placeholder, textarea::placeholder{ color:var(--text-ghost); }
input:focus, select:focus, textarea:focus{
  outline:none;
  border-color:var(--acc);
  background-color:rgba(var(--acc-rgb),.06);
  box-shadow:var(--ring-acc), var(--glow-acc);
}
/* the field itself owns the focus ring, so the browser's default one
   on top of it would be a second ring saying the same thing */
input:focus-visible, select:focus-visible, textarea:focus-visible{ outline:none; }
select{ cursor:pointer; }
select option{ background:var(--surface); color:var(--text); }
input[aria-invalid="true"], textarea[aria-invalid="true"]{ border-color:var(--red); }

/* The rail is 2px. The CONTROL is not: the element box is the drag
   target, so a 2px-tall input is a slider you have to hit within two
   pixels to move at all. The box gets a real height and stays
   transparent; the 2px line is drawn by the track pseudo-element. */
input[type="range"]{
  appearance:none; -webkit-appearance:none;
  height:20px; width:100%;
  background:transparent;
  border:0;
  border-radius:0;
  cursor:pointer;
  padding:0; margin:0;
  touch-action:none;                  /* a drag on it is a drag, not a scroll */
}
input[type="range"]::-webkit-slider-runnable-track{
  height:2px;
  background:var(--line-strong);
  border:0; border-radius:0;
}
input[type="range"]::-moz-range-track{
  height:2px;
  background:var(--line-strong);
  border:0; border-radius:0;
}
input[type="range"]::-webkit-slider-thumb{
  appearance:none; -webkit-appearance:none;
  width:12px; height:12px;
  margin-top:-5px;                    /* centre the thumb on the 2px rail */
  border-radius:0;                    /* square, like everything else */
  background:var(--acc);
  border:0;
  cursor:pointer;
}
input[type="range"]::-moz-range-thumb{
  width:12px; height:12px;
  border-radius:0;
  background:var(--acc);
  border:0;
  cursor:pointer;
}
/* the generic input:focus ring matches a range too, and on a control
   whose box is 20px of empty space around a 2px rail it draws a fat
   rectangle around nothing. The thumb carries the focus here. */
input[type="range"]:focus{
  outline:none;
  background:transparent;
  border:0;
  box-shadow:none;
}
input[type="range"]:focus-visible{ outline:none; }
input[type="range"]:focus-visible::-webkit-slider-thumb{ box-shadow:0 0 0 3px rgba(var(--acc-rgb),.45); }
input[type="range"]:focus-visible::-moz-range-thumb{ box-shadow:0 0 0 3px rgba(var(--acc-rgb),.45); }

/* the accent as a native control colour, so a checkbox and a radio
   are not the two things on the page wearing the browser's blue */
input[type="checkbox"], input[type="radio"]{ accent-color:var(--acc); cursor:pointer; }

/* ==================================================================
   MENU  —  a dropdown / popover
   The active row is marked on its LEADING EDGE, not by a tick glyph
   and not by a filled row. On --overlay: a menu has to read over
   whatever the page happens to have under it, so it is the one
   surface in the system that is nearly opaque — and it gets there
   through a token, so .fx-lite still reaches it.
   ================================================================== */
.menu{
  min-width:240px;
  background-color:var(--overlay);
  border:var(--hair) solid var(--line-strong);
  box-shadow:var(--shadow-2);
  padding:var(--s-1) 0;
  backdrop-filter:var(--overlay-blur);
  -webkit-backdrop-filter:var(--overlay-blur);
}
.menu[hidden]{ display:none; }
.menu__head{
  padding:var(--s-3) var(--s-4) var(--s-2);
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:var(--text-ghost);
  border-bottom:var(--hair) solid var(--line);
  margin-bottom:var(--s-1);
}
.menu__option{
  appearance:none; width:100%; text-align:left;
  background:none;
  display:flex; align-items:center; gap:var(--s-3);
  padding:var(--s-3) var(--s-4);
  font-family:var(--mono);
  font-size:var(--fs-chrome-md); letter-spacing:var(--tr-chrome-md); text-transform:uppercase;
  color:var(--text-dim);
  cursor:pointer;
  border:0; border-left:2px solid transparent;
  transition:color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
  white-space:nowrap;
}
.menu__option:hover{
  color:var(--text);
  background-color:rgba(var(--chrome-rgb),.08);
  border-left-color:var(--chrome);
}
.menu__option.is-active,
.menu__option[aria-selected="true"]{
  color:var(--acc);
  background-color:rgba(var(--acc-rgb),.06);
  border-left-color:var(--acc);
  font-weight:600;
}
.menu__option:focus-visible{ outline-offset:-3px; }

/* ==================================================================
   CARD  —  a tile of content, with the corner brackets
   ------------------------------------------------------------------
   The brackets are --chrome and appear on hover/focus only. They are
   the frame that tells a curated set from a loose grid; without them
   a group of cards was indistinguishable from the grid under it.

   The card floats on --glass. It used to be an opaque --surface fill,
   which was the sheet quietly breaking its own step 5 ("Does it sit
   on the page? -> --glass. Never an opaque fill.") in the component
   that covers the most area of any real screen. Cards are most of
   what a gallery IS, so an opaque card meant rule 3 barely applied
   to the page a visitor actually looks at.
   ================================================================== */
.card-grid{
  list-style:none; padding:0; margin:0;
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(100%, 260px), 1fr));
  gap:var(--s-4);
}
.card{
  position:relative;
  background-color:var(--glass);
  border:var(--hair) solid var(--line);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
  margin:0;
  /* `scale` is in the list on purpose: a press cue drives the
     INDEPENDENT scale property (it must — the card can carry a hover
     translate at the same time), and a property not listed here does
     not transition at all. */
  transition:background-color var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease),
             box-shadow var(--dur-2) var(--ease), translate var(--dur-2) var(--ease), scale var(--dur-2) var(--ease);
}
.card::before,
.card::after{
  content:"";
  position:absolute;
  width:14px; height:14px;
  border:var(--hair) solid var(--chrome);
  opacity:0; pointer-events:none; z-index:5;
  transition:opacity var(--dur-2) var(--ease);
}
.card::before{ top:-1px; left:-1px; border-right:0; border-bottom:0; }
.card::after { bottom:-1px; right:-1px; border-left:0; border-top:0; }
.card:hover{
  background-color:var(--glass-hi);
  border-color:var(--line-strong);
  translate:0 -2px;
  box-shadow:var(--shadow-1);
}
.card:active{ scale:.995; }
.card:hover::before, .card:hover::after,
.card:focus-within::before, .card:focus-within::after{ opacity:1; }
.card > a{ display:flex; flex-direction:column; color:var(--text); height:100%; }
.card > a:hover{ color:var(--text); }
.card > a:focus-visible{ outline-offset:-3px; }

.card__img{ position:relative; aspect-ratio:3/2; overflow:hidden; background:var(--bg-2); }
.card__img img{
  width:100%; height:100%; object-fit:cover; display:block;
  filter:saturate(.85) brightness(.9);
  transition:scale var(--dur-3) var(--ease), filter var(--dur-3) var(--ease);
}
.card:hover .card__img img{ scale:1.04; filter:saturate(1) brightness(1); }
.card__img::after{
  content:""; position:absolute; inset:0;
  background:linear-gradient(to top, rgba(0,0,0,.45) 0%, transparent 45%);
  pointer-events:none;
}
/* a chip laid ON the image: --scrim, not --glass */
.card__idx{
  position:absolute; top:var(--s-3); left:var(--s-3); z-index:3;
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-sm);
  color:var(--text);
  background-color:var(--scrim);
  backdrop-filter:var(--scrim-blur);
  -webkit-backdrop-filter:var(--scrim-blur);
  border:var(--hair) solid var(--line);
  padding:var(--s-0) var(--s-2);
  pointer-events:none;
}
.card__hover{
  position:absolute; inset:0; z-index:2;
  display:flex; align-items:center; justify-content:center;
  opacity:0; pointer-events:none;
  background:linear-gradient(to top, rgba(0,0,0,.5) 0%, rgba(0,0,0,.1) 45%, transparent 75%);
  transition:opacity var(--dur-2) var(--ease);
}
.card__hover span{
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:var(--chrome);
  border:var(--hair) solid var(--chrome);
  background-color:var(--scrim);
  backdrop-filter:var(--scrim-blur);
  -webkit-backdrop-filter:var(--scrim-blur);
  padding:var(--s-3) var(--s-5);
  translate:0 6px;
  transition:translate var(--dur-2) var(--ease-out);
}
.card:hover .card__hover, .card:focus-within .card__hover{ opacity:1; }
.card:hover .card__hover span, .card:focus-within .card__hover span{ translate:0 0; }
/* the caption is read, so it takes the lifted step: reading copy is
   the one thing that never negotiates with the backdrop */
.card__cap{
  padding:var(--s-4);
  display:flex; flex-direction:column; gap:var(--s-2);
  border-top:var(--hair) solid var(--line);
  background-color:var(--glass-hi);
}
.card__name{ font-size:var(--fs-text-lg); font-weight:600; letter-spacing:var(--tr-title); line-height:1.25; color:var(--text); word-break:break-word; }
.card__meta{
  display:flex; align-items:baseline; gap:var(--s-3); justify-content:space-between;
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-sm); text-transform:uppercase;
  color:var(--text-dim);
  font-variant-numeric:tabular-nums;
}
.card__meta .date{ color:var(--text-dim); white-space:nowrap; flex-shrink:0; }
/* the placeholder when a card has no image: hatching, not an icon */
.card__empty{
  width:100%; height:100%;
  background:
    repeating-linear-gradient(45deg, rgba(255,255,255,.02) 0 6px, transparent 6px 12px),
    var(--bg-2);
  display:flex; align-items:center; justify-content:center;
  color:var(--text-ghost);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
}

/* ==================================================================
   PANEL AND KEY/VALUE  —  a read-out of facts
   The stack is ONE pane -- a fill, a blur and a hairline (rule 3) --
   and its rows are separated by hairlines of their own, not a set of
   floating boxes with their own borders.

   It used to draw those hairlines as a one-hair gap over a --line
   background. That only works when every row is opaque. A .set row has
   no fill at all and .panel is --glass-hi, so the 9% white behind them
   came through every row and the whole stack read as a lighter box:
   depth from a lighter fill, the one thing rule 3 rules out.
   ================================================================== */
.stack{
  display:flex; flex-direction:column;
  background-color:var(--glass);
  border:var(--hair) solid var(--line);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
  width:100%;
}
.stack > * + *{ border-top:var(--hair) solid var(--line); }
.panel{
  background-color:var(--glass-hi);
  padding:var(--s-5);
  width:100%; min-width:0;
}
.panel h2{
  margin:0 0 var(--s-4);
  font-family:var(--sans);
  font-size:var(--fs-text-lg); font-weight:600; letter-spacing:var(--tr-head);
  color:var(--text-head);
  border-bottom:var(--hair) solid var(--line);
  padding-bottom:var(--s-3);
  display:flex; align-items:center; gap:var(--s-3);
}
.panel h2 > i{ color:var(--acc); font-size:var(--icon-md); }
.kv{
  display:grid;
  grid-template-columns:110px minmax(0, 1fr);
  gap:var(--s-2) var(--s-4);
  margin:0;
  font-size:var(--fs-text-md);
  overflow-wrap:anywhere;
}
.kv dt{
  font-size:var(--fs-text-sm); letter-spacing:var(--tr-text);
  color:var(--text-dim);
  align-self:center;
  word-break:normal; overflow-wrap:break-word; hyphens:auto;
}
.kv dd{ margin:0; color:var(--text); font-size:var(--fs-text-sm); }
/* a MEASURED value is furniture and gets the mono voice; a value a
   person reads (a name, a sentence) does not */
.kv dd.num{
  font-family:var(--mono); font-size:var(--fs-chrome-md);
  letter-spacing:var(--tr-num); color:var(--label);
  font-variant-numeric:tabular-nums;
}
.kv dd a{ color:var(--acc); }
.kv dd a:hover{ text-decoration:underline; }

/* ==================================================================
   STATUS
   ------------------------------------------------------------------
   Never colour alone. A dot that means "live" to a sighted visitor
   must mean it to everyone, so .status carries the word and the dot
   is the ornament — not the other way round.
   ================================================================== */
.dot{
  display:inline-block;
  width:7px; height:7px;
  border-radius:50%;              /* one of the few things that is round */
  background:var(--text-ghost);
  vertical-align:middle;
  flex:0 0 auto;
}
.dot--ok  { background:var(--ok);  box-shadow:0 0 8px var(--ok); }
.dot--amb { background:var(--amb); box-shadow:0 0 8px var(--amb); }
.dot--red { background:var(--red); box-shadow:0 0 8px var(--red); }
.status{
  display:inline-flex; align-items:center; gap:var(--s-3);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:var(--text-dim);
}

/* ==================================================================
   HUD FRAME  —  the viewfinder idiom
   ------------------------------------------------------------------
   Corner brackets, a thirds grid and a reticle over a live surface.
   This belongs where the screen IS an instrument (a capture view, a
   live monitor). Do not lift the blinking dot and the reticle into
   ordinary page chrome.
   ================================================================== */
.hud{ position:relative; overflow:hidden; background:var(--bg-2); }
.hud__corner{
  position:absolute;
  width:26px; height:26px;
  border:var(--hair) solid rgba(var(--chrome-rgb),.75);
  opacity:.9; pointer-events:none; z-index:4;
}
.hud__corner--tl{ top:var(--s-5); left:var(--s-5);  border-right:0; border-bottom:0; }
.hud__corner--tr{ top:var(--s-5); right:var(--s-5); border-left:0;  border-bottom:0; }
.hud__corner--bl{ bottom:var(--s-5); left:var(--s-5);  border-right:0; border-top:0; }
.hud__corner--br{ bottom:var(--s-5); right:var(--s-5); border-left:0;  border-top:0; }
.hud__thirds{
  position:absolute; inset:0; z-index:3; pointer-events:none;
  background-image:
    linear-gradient(to right, rgba(255,255,255,.055) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(255,255,255,.055) 1px, transparent 1px);
  background-size:33.334% 33.334%;
  background-position:-1px -1px;
}
.hud__reticle{
  position:absolute; left:50%; top:50%; z-index:3;
  width:58px; height:58px;
  /* the centring lives on translate, NOT on transform: a shared
     keyframe or an :active rule that touches transform would throw a
     centred control across the screen */
  translate:-50% -50%;
  border:var(--hair) solid rgba(255,255,255,.22);
  opacity:.55; pointer-events:none;
  transition:border-color var(--dur-2) var(--ease), opacity var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.hud__reticle::before,
.hud__reticle::after{ content:""; position:absolute; left:50%; top:50%; background:rgba(255,255,255,.3); }
.hud__reticle::before{ width:1px; height:10px; translate:-50% -50%; }
.hud__reticle::after { width:10px; height:1px; translate:-50% -50%; }
.hud.is-busy .hud__reticle{
  border-color:var(--acc);
  opacity:1;
  box-shadow:0 0 18px rgba(var(--acc-rgb),.35), inset 0 0 12px rgba(var(--acc-rgb),.15);
}
.hud__bar{
  position:absolute; left:0; right:0; z-index:5;
  display:flex; align-items:center; gap:var(--s-5);
  padding:var(--s-5);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:var(--text-dim);
  pointer-events:none;
}
.hud__bar--top{ top:0; }
.hud__bar--bottom{ bottom:0; }
.hud__bar b{ color:var(--text); font-weight:400; font-variant-numeric:tabular-nums; }
.hud__bar .spacer{ margin-left:auto; }
.hud__rec{ display:inline-flex; align-items:center; gap:var(--s-3); color:#e8908e; text-shadow:0 0 10px rgba(var(--red-rgb),.4); }
.hud__rec .dot{ background:var(--red); box-shadow:0 0 10px var(--red); animation:hud-blink 1.6s steps(2,start) infinite; }
@keyframes hud-blink{ 50%{ opacity:.15; } }
.fx-lite .hud__rec .dot{ animation:none; }
@media (prefers-reduced-motion: reduce){ .hud__rec .dot{ animation:none; } }

/* ==================================================================
   FOOTER
   ================================================================== */
.foot{
  position:relative; z-index:10;
  margin-top:var(--s-8);
  border-top:var(--hair) solid var(--line);
  background-color:var(--pane);
  backdrop-filter:var(--pane-blur);
  -webkit-backdrop-filter:var(--pane-blur);
}
.foot__inner{
  max-width:var(--page-max);
  margin:0 auto;
  padding:var(--s-6) var(--page-pad);
  display:flex; align-items:center; justify-content:space-between;
  gap:var(--s-6); flex-wrap:wrap;
  font-size:var(--fs-text-sm); letter-spacing:var(--tr-text);
  color:var(--text-ghost);
}
.foot__brand{ display:flex; align-items:center; gap:var(--s-3); color:var(--text-dim); }
.foot__brand img{ height:18px; opacity:.85; }
.foot__links{ display:flex; gap:var(--s-5); align-items:center; flex-wrap:wrap; }
.foot__links a{ color:var(--text-dim); border-bottom:var(--hair) solid transparent; padding-bottom:var(--hair); transition:color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease); }
.foot__links a:hover{ color:var(--acc); border-bottom-color:var(--acc); }
.foot__meta b{ color:var(--text-dim); font-weight:400; }

/* The classic 88x31 web buttons. The art is fixed-size pixel work: shown at
   exactly 88x31, never smoothed, with the hairline outside the picture. A
   button may be wrapped in a link. (88, 31 and the pixel rendering are the
   format itself, not sizes off the scale.) */
.foot__badges{ display:flex; gap:var(--s-3); align-items:center; flex-wrap:wrap; }
.foot__badges a{ display:block; }
.foot__badges img{
  display:block;
  width:88px; height:31px;
  box-sizing:content-box;
  border:var(--hair) solid var(--line);
  image-rendering:pixelated;
}

/* Phones: the desktop row is not tipped on its side. One block per line
   reads as ragged fragments shoved into the left margin, so the footer is
   sorted into rows instead, as the gallery does it:

     [ operator card ]   [88x31] [88x31] [88x31]
     ——————————————————————————————————————————
     brand                               meta
                                        links

   The tiles (the optional operator card and the buttons) make one row, a
   rule runs under them, the brand and the colophon share the next, and the
   links get a full-width row of their own so a third one never wraps alone.
   Every cell is placed by hand, so a missing block shifts nothing. Without
   an operator card the buttons take the whole first row. The buttons shrink
   evenly from 88px rather than stacking, and keep their ratio. */
@media (max-width: 760px){
  .foot__inner{
    display:grid;
    grid-template-columns:auto auto;
    justify-content:space-between;
    align-items:center;
    gap:0 var(--s-4);
    padding:var(--s-5) var(--page-pad);
  }
  .foot__inner:has(.foot__badges, .foot__op)::before{
    content:"";
    grid-area:2 / 1 / 3 / 3;
    height:var(--hair);
    background:var(--line);
    margin:var(--s-5) 0 var(--s-4);
  }
  .foot__op    { grid-area:1 / 1; justify-self:start; min-width:0; }
  .foot__badges{ grid-area:1 / 2; justify-self:stretch; justify-content:flex-end; flex-wrap:nowrap; min-width:0; }
  .foot__inner:not(:has(.foot__op)) .foot__badges{ grid-column:1 / 3; justify-content:flex-start; }
  .foot__badges > *{ flex:0 1 88px; min-width:0; }
  .foot__badges img{ box-sizing:border-box; width:100%; height:auto; }
  .foot__brand { grid-area:3 / 1; }
  .foot__meta  { grid-area:3 / 2; justify-self:end; text-align:right; }
  .foot__links { grid-area:4 / 1 / 5 / 3; justify-self:end; justify-content:flex-end; align-items:baseline; gap:var(--s-2) var(--s-4); padding-top:var(--s-4); }
}

/* The optional landing-page colophon. The oversized wordmark repeats the
   brand; it is decorative, aria-hidden, and never carries links or facts. */
.foot--editorial{ overflow:hidden; }
.foot__floor{
  margin:0;
  padding:var(--s-6) var(--page-pad) 0;
  font-family:var(--display-font);
  font-weight:400;
  font-size:var(--fs-brand-floor);
  letter-spacing:var(--tr-brand-floor);
  line-height:1.1;
  white-space:nowrap;
  text-align:center;
  color:var(--chrome);
  opacity:var(--brand-floor-opacity);
  user-select:none;
  pointer-events:none;
}
@media (prefers-contrast: more){
  :root{ --brand-floor-opacity: .4; }
}
@media (forced-colors: active){
  .foot__floor{ color:CanvasText; opacity:1; }
}


/* Scroll-driven brand aperture. A black SVG curtain with the original
   mark cut out; the host page owns the sticky scene and scroll geometry. */
.splash{ position:fixed; inset:0; z-index:150; pointer-events:none; }
.splash__svg{ position:absolute; inset:0; width:100%; height:100%; }
.splash__outline{ fill:var(--title-a); stroke:var(--chrome); stroke-width:var(--hair); }
.splash__outline path{ vector-effect:non-scaling-stroke; }
.splash__controls{ position:absolute; inset:var(--s-6) var(--page-pad) auto; display:flex; align-items:center; justify-content:space-between; gap:var(--s-5); pointer-events:auto; }
.splash__brand{ font-family:var(--display-font); font-size:calc(var(--fs-text-xl) * var(--display-scale)); letter-spacing:var(--tr-brand-floor); color:var(--text); }
.splash__ruler{ position:absolute; visibility:hidden; width:var(--splash-initial-width); height:0; }
.splash__hint{ position:absolute; inset:auto var(--page-pad) var(--s-8); display:flex; align-items:center; justify-content:center; gap:var(--s-4); margin:0; color:var(--label); font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-sm); }
.fx-lite .splash{ display:none; }
@media (prefers-reduced-motion:reduce), (prefers-reduced-transparency:reduce), (forced-colors:active){
  .splash{ display:none; }
}

/* ==================================================================
   THE MARK
   ------------------------------------------------------------------
   The house wing, as INLINE SVG. An <img> is a closed document: nothing
   outside it can reach a path, which means no stylesheet of ours can
   fill it, draw it, or split it into colour. Every app that shows the
   mark in its bar and on its door was copying this block by hand.

   One drawing, three instances, and the difference between them is a
   HEIGHT and whether they carry the ghosts:

     .mark--nav    the app bar, splits on hover
     .mark--door   the sign-in card, the same
     .mark--foot   the colophon: a stamp, not a control, and still

   The resting state of every one of them is the SOLID mark. A hollow
   outline is a state a script asks for, never the default — a sheet
   that starts the logo invisible is one failed script away from a
   header with a hole in it.

   The markup is eight <path>s in a viewBox, twice more when it carries
   ghosts; whoever brings the mark brings that file.
   ================================================================== */
.mark{
  display:block; position:relative;
  /* the artboard's own ratio, so a variant only ever sets a height */
  aspect-ratio:964 / 543;
  flex:0 0 auto;
}
.mark__svg{
  position:absolute; inset:0;
  width:100%; height:100%;
  /* the ghosts sit a couple of units off the mark and must not be cut */
  overflow:visible;
}
.piece{ fill:#ffffff; stroke:none; }

/* ---- the chromatic split ----------------------------------------
   Two whole copies of the wing, offset and blended to screen. The two
   hexes are NOT palette colours and are not allowed to become any:
   they are the accent's two flanks, one toward blue and one toward
   violet, which is what a split signal does to an edge. They appear
   only on a COPY of the mark — never on a surface, a label or a
   control. */
.mark__ghost{
  position:absolute; inset:0;
  pointer-events:none;
  opacity:0;
  mix-blend-mode:screen;
  will-change:translate, opacity;
}
.mark__ghost .piece--ghost{ stroke-width:1; }
.mark__ghost--r .piece--ghost{ fill:#7aa6ff; stroke:#7aa6ff; }
.mark__ghost--b .piece--ghost{ fill:#b478ff; stroke:#b478ff; }

.mark--nav { height:28px; }
.mark--door{ height:28px; }
.mark--foot{ height:18px; opacity:.85; }
.mark--nav, .mark--door{
  filter:drop-shadow(0 0 12px rgba(var(--acc-rgb),.35));
  transition:filter var(--dur-2) var(--ease);
}

/* The hover: the signal splits and comes back. Behind (hover: hover) on
   the house rule — a hover that REPAINTS costs the first tap on a touch
   screen. `translate`, never `transform`, so it can never overwrite a
   transform something else put on the same element. */
@media (hover: hover){
  .mark--nav:hover, .mark--door:hover{
    filter:drop-shadow(0 0 16px rgba(var(--acc-rgb),.55));
  }
  .mark--nav:hover .mark__ghost--r,
  .mark--door:hover .mark__ghost--r{ animation:markSplitR var(--mark-split) steps(4, end); }
  .mark--nav:hover .mark__ghost--b,
  .mark--door:hover .mark__ghost--b{ animation:markSplitB var(--mark-split) steps(4, end); }
  .mark--nav:hover .mark__svg,
  .mark--door:hover .mark__svg{ animation:markJitter var(--mark-split) steps(4, end); }
}

/* A magnified contour quotes the real mark's paths, with no fill.
   Its containing composition owns the crop and the magnification.
   It is decorative, never a replacement for the complete house mark. */
.mark-detail{ position:absolute; pointer-events:none; opacity:var(--mark-detail-opacity); }
.mark-detail__svg{ display:block; width:100%; height:auto; overflow:visible; }
.mark-detail .piece{ fill:none; stroke:var(--chrome); stroke-width:var(--hair); vector-effect:non-scaling-stroke; }
@media (prefers-contrast:more), (forced-colors:active){
  :root{ --mark-detail-opacity:0; }
}

/* The feature mark opens its wings when its real navigation link is
   hovered or focused. Resting geometry stays exactly the house drawing. */
.mark--feature .piece{ transition:translate var(--dur-3) var(--ease-out); }
@media (hover:hover) and (prefers-reduced-motion:no-preference){
  html:not(.fx-lite) .mark-link:hover .mark--feature [data-piece="wing-l"],
  html:not(.fx-lite) .mark-link:focus-visible .mark--feature [data-piece="wing-l"]{ translate:calc(var(--mark-spread) * -1) calc(var(--mark-lift) * -1); }
  html:not(.fx-lite) .mark-link:hover .mark--feature [data-piece="wing-r"],
  html:not(.fx-lite) .mark-link:focus-visible .mark--feature [data-piece="wing-r"]{ translate:var(--mark-spread) calc(var(--mark-lift) * -1); }
}

/* A closing brand signature can quote the same split, at its own scale. */
.mark--signature{ width:min(38%, calc(var(--s-11) * 3)); height:auto; }
@media (hover:hover) and (prefers-reduced-motion:no-preference){
  html:not(.fx-lite) .mark--signature:hover .mark__ghost--r{ animation:markSplitR var(--mark-split) steps(4, end); }
  html:not(.fx-lite) .mark--signature:hover .mark__ghost--b{ animation:markSplitB var(--mark-split) steps(4, end); }
  html:not(.fx-lite) .mark--signature:hover > .mark__svg{ animation:markJitter var(--mark-split) steps(4, end); }
}

/* steps(), not a curve: a signal that slides apart smoothly is a slide.
   The split has to arrive in frames to read as a glitch. */
@keyframes markSplitR{
  0%  { opacity:0;   translate:0 0; }
  20% { opacity:.85; translate:-4px 1px; }
  55% { opacity:.7;  translate:-2px -1px; }
  80% { opacity:.5;  translate:-1px 0; }
  100%{ opacity:0;   translate:0 0; }
}
@keyframes markSplitB{
  0%  { opacity:0;   translate:0 0; }
  20% { opacity:.85; translate:4px -1px; }
  55% { opacity:.7;  translate:2px 1px; }
  80% { opacity:.5;  translate:1px 0; }
  100%{ opacity:0;   translate:0 0; }
}
@keyframes markJitter{
  0%, 100%{ translate:0 0; }
  20%     { translate:1px 0; }
  55%     { translate:-1px 0; }
  80%     { translate:.5px 0; }
}
/* Someone who asked for less motion still gets the mark, and gets it
   whole — just none of the three above. */
@media (prefers-reduced-motion: reduce){
  .mark__ghost{ animation:none !important; opacity:0 !important; }
  .mark__svg{ animation:none !important; }
}
.fx-lite .mark__ghost{ animation:none !important; opacity:0 !important; }

/* ==================================================================
   AVATAR  —  who this is, in a square
   ------------------------------------------------------------------
   Initials, or a picture, in the same box. It carries NO accent: an
   avatar is not a state, it is an identity (rule 1), so the frame is
   furniture and the letters are in the sans voice — they are a NAME,
   which is data, and data is not cased or tracked (rule 4).
   ================================================================== */
.avatar{
  display:inline-grid; place-items:center;
  width:var(--s-8); height:var(--s-8);
  flex:0 0 auto;
  border:var(--hair) solid var(--line-strong);
  background-color:rgba(var(--label-rgb),.08);
  border-radius:var(--radius);
  color:var(--text);
  font-family:var(--sans);
  font-size:var(--fs-text-lg); font-weight:600; letter-spacing:var(--tr-head);
  padding:0;
  overflow:hidden;
}
.avatar img{ width:100%; height:100%; object-fit:cover; }
.avatar--sm{ width:var(--s-7); height:var(--s-7); font-size:var(--fs-text-sm); }
.avatar--lg{ width:var(--s-9); height:var(--s-9); font-size:var(--fs-text-xl); }
/* An avatar that opens something is a control, and says so when it is open. */
.avatar--btn{ cursor:pointer; transition:border-color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease); }
.avatar--btn[aria-expanded="true"]{ border-color:var(--acc); background-color:rgba(var(--acc-rgb),.16); }
@media (hover: hover){
  .avatar--btn:hover{ border-color:var(--chrome); background-color:rgba(var(--chrome-rgb),.10); }
}

/* ==================================================================
   SWITCH  —  one setting, on or off
   ------------------------------------------------------------------
   .seg answers "pick one of these". This answers "is this on", which
   is the other half of every settings screen and had no idiom.

   On is a STATE, so on is the accent — as a fill, with --on-acc on
   the knob, which is what --acc is derived to survive. And never hue
   alone (rule 6): the word beside it says On or Off, and the control
   itself is a real <button role="switch" aria-checked>, so a screen
   reader reads the same thing the word does.
   ================================================================== */
.switch-wrap{ display:inline-flex; align-items:center; gap:var(--s-3); }
.switch__word{
  min-width:3ch; text-align:right;
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-sm); text-transform:uppercase;
  color:var(--text-dim);
}
.switch{
  appearance:none;
  position:relative;
  width:calc(var(--s-7) + var(--s-3));
  height:calc(var(--s-5) + var(--s-1));
  padding:0;
  border:var(--hair) solid var(--line-strong);
  border-radius:var(--radius);
  background-color:var(--surface);
  cursor:pointer;
  transition:background-color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease);
}
/* The TARGET is --tap; the control is not. Growing the switch itself on a
   phone gives a 44px-tall track with a 12px knob rattling around inside it,
   so the box stays the size it looks and the reach is a transparent margin
   around it. */
.switch::before{
  content:"";
  position:absolute; inset:calc(var(--s-4) * -1);
}
.switch::after{
  content:"";
  position:absolute; top:50%; left:var(--s-1);
  width:var(--s-4); height:var(--s-4);
  translate:0 -50%;
  background-color:var(--text-dim);
  /* `left`, not a transform: the knob is centred with translate already,
     and two of them on one element is how a control ends up off screen */
  transition:left var(--dur-2) var(--ease-out), background-color var(--dur-1) var(--ease);
}
.switch[aria-checked="true"]{ background-color:var(--acc); border-color:var(--acc); }
.switch[aria-checked="true"]::after{ left:calc(100% - var(--s-4) - var(--s-1)); background-color:var(--on-acc); }
.switch:disabled{ opacity:.45; cursor:not-allowed; }
.switch-wrap:has(.switch[aria-checked="true"]) .switch__word{ color:var(--text); }
@media (hover: hover){
  .switch:hover{ border-color:var(--chrome); }
  .switch[aria-checked="true"]:hover{ border-color:var(--acc-soft); }
}

/* ==================================================================
   METER  —  how much of something there is
   ------------------------------------------------------------------
   Rule 1 licenses the accent for "progress fills" and the sheet had
   no fill to put it on. A track in --line-strong, a fill in --acc,
   and the value itself comes from a custom property so a strict CSP
   can set it: no inline style attribute, ever (step 4.5).

     <span class="meter" role="meter" aria-valuenow="64" …>
       <span class="meter__fill"></span></span>
     el.style.setProperty('--p', 64)   // CSSOM, allowed

   The number beside it is not optional: a bar with no figure is a
   mood, and a screen reader reads aria-valuenow, not a width.
   ================================================================== */
.meter{
  display:block; position:relative;
  height:var(--s-1);
  background-color:var(--line-strong);
  border-radius:var(--radius);
  overflow:hidden;
}
.meter__fill{
  display:block; height:100%;
  width:calc(var(--p, 0) * 1%);
  background-color:var(--acc);
  transition:width var(--dur-3) var(--ease-out);
}
.meter--amb .meter__fill{ background-color:var(--amb); }
.meter--red .meter__fill{ background-color:var(--red); }

/* ==================================================================
   SETTING ROW  —  a thing, where it stands, and what to do about it
   ------------------------------------------------------------------
   The row every settings screen is made of, inside a .stack so the
   rows are hairline-separated rather than a pile of boxes:

     icon | name + one line of prose | status and controls

   The name is read, so it is sans; the icon box is furniture; the
   aside holds at most one --primary. On a narrow screen the aside
   drops to its own line rather than squeezing the prose to nothing.
   ================================================================== */
.set{
  display:grid;
  grid-template-columns:auto minmax(0, 1fr) auto;
  gap:var(--s-4) var(--s-5);
  align-items:center;
  transition:background-color var(--dur-2) var(--ease), box-shadow var(--dur-2) var(--ease);
}
.set--top{ align-items:start; }
.set__ico{
  display:grid; place-items:center;
  width:var(--s-7); height:var(--s-7);
  border:var(--hair) solid var(--line);
  color:var(--label);
  font-size:var(--fs-chrome-lg);
}
.set__ico--amb{ color:var(--amb); border-color:rgba(var(--amb-rgb),.4); }
.set__ico--red{ color:var(--red); border-color:rgba(var(--red-rgb),.4); }
.set__ico img{ width:70%; height:auto; }
.set__text{ display:flex; flex-direction:column; gap:var(--s-1); min-width:0; }
.set__t{
  display:flex; align-items:center; flex-wrap:wrap; gap:var(--s-3);
  font-size:var(--fs-text-lg); font-weight:600; letter-spacing:var(--tr-head);
  color:var(--text); line-height:1.3;
}
.set__d{ font-size:var(--fs-text-sm); color:var(--text-dim); line-height:1.5; }
.set__aside{ display:flex; align-items:center; justify-content:flex-end; gap:var(--s-4); flex-wrap:wrap; }
/* A row something has just pointed at — a search hit, a deep link. That IS
   a state, and it is a brief one: the caller takes the class off again. */
.set.is-found{ box-shadow:inset 2px 0 0 var(--acc); background-color:rgba(var(--acc-rgb),.06); }

/* ==================================================================
   TABLE  —  rows of facts
   ------------------------------------------------------------------
   The two voices, one component: the head is mono furniture, the
   cells are what a person reads, and a measured column takes .num and
   goes mono with tabular figures so the digits line up down the page.
   Hairlines between rows, none around the table; wrap it in something
   scrollable, because a table is the one thing that cannot reflow.
   ================================================================== */
.tbl-wrap{ overflow-x:auto; }
.tbl{ width:100%; border-collapse:collapse; font-size:var(--fs-text-md); }
.tbl th{
  text-align:left;
  padding:var(--s-3) var(--s-5);
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); font-weight:400; letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:var(--label);
  border-bottom:var(--hair) solid var(--line);
  white-space:nowrap;
}
.tbl td{
  padding:var(--s-4) var(--s-5);
  border-bottom:var(--hair) solid var(--line);
  vertical-align:middle;
  color:var(--text-dim);
}
.tbl td:first-child{ color:var(--text); }
.tbl tr:last-child td{ border-bottom:0; }
.tbl td.num{
  font-family:var(--mono); font-size:var(--fs-chrome-md);
  letter-spacing:var(--tr-num); color:var(--label);
  white-space:nowrap;
}
.tbl .r{ text-align:right; }

/* ==================================================================
   DIALOG  —  a question that has to be answered here
   ------------------------------------------------------------------
   A <dialog>, so the browser owns the top layer, the focus trap and
   Escape. On --overlay like a menu: it must read over whatever the
   page happens to have under it.

   It is not a place to put a screen. If it needs a heading and a
   scrollbar and three sections, it is a page.
   ================================================================== */
.dlg{
  width:min(calc(100% - 2 * var(--page-pad)), var(--door-w));
  max-width:none;
  padding:0;
  color:var(--text);
  background-color:var(--overlay);
  backdrop-filter:var(--overlay-blur);
  -webkit-backdrop-filter:var(--overlay-blur);
  border:var(--hair) solid var(--line-strong);
  border-radius:var(--radius);
  box-shadow:var(--shadow-2);
}
.dlg::backdrop{
  background-color:rgba(0,0,0,.6);
  backdrop-filter:var(--blur-1);
  -webkit-backdrop-filter:var(--blur-1);
}
.dlg[open]{ animation:dlg-in var(--dur-2) var(--ease-out); }
@keyframes dlg-in{ from{ opacity:0; translate:0 var(--s-3); } }
.fx-lite .dlg[open]{ animation:none; }
@media (prefers-reduced-motion: reduce){ .dlg[open]{ animation:none; } }

.dlg__head{
  position:relative;
  display:flex; flex-direction:column; gap:var(--s-1);
  padding:var(--s-5) var(--s-5) var(--s-4);
  border-bottom:var(--hair) solid var(--line);
}
.dlg__eyebrow{
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:var(--label);
}
.dlg__t{ margin:0; font-size:var(--fs-text-xl); font-weight:600; letter-spacing:var(--tr-head); overflow-wrap:anywhere; }
.dlg__x{ position:absolute; top:var(--s-3); right:var(--s-3); }
.dlg__body{ padding:var(--s-5); }
.dlg__fields{ display:flex; flex-direction:column; gap:var(--s-4); }
.dlg__p{ margin:0; color:var(--text-dim); font-size:var(--fs-text-md); }
.dlg__foot{
  display:flex; justify-content:flex-end; gap:var(--s-3); flex-wrap:wrap;
  padding:var(--s-4) var(--s-5);
  border-top:var(--hair) solid var(--line);
}

/* ==================================================================
   TOAST  —  that happened
   ------------------------------------------------------------------
   A line that says what was just done, and goes away. It reports; it
   does not mark a state, so its edge is --chrome and NOT the accent
   (rule 1). The variants are statuses, the same licence a field error
   has, and each carries a glyph as well as its colour (rule 6).

   It lives in a region the page announces (role="status",
   aria-live="polite"), and it is never the only place an outcome is
   shown: a message that vanishes has not told anyone who was reading
   something else.
   ================================================================== */
.toasts{
  position:fixed; z-index:200;
  right:var(--page-pad); top:calc(var(--nav-h) + var(--s-4));
  display:flex; flex-direction:column; align-items:flex-end; gap:var(--s-3);
  pointer-events:none;
}
.toast{
  display:flex; align-items:center; gap:var(--s-3);
  max-width:min(calc(100vw - 2 * var(--page-pad)), var(--door-w));
  padding:var(--s-3) var(--s-4);
  background-color:var(--overlay);
  backdrop-filter:var(--overlay-blur);
  -webkit-backdrop-filter:var(--overlay-blur);
  border:var(--hair) solid var(--line-strong);
  border-left:2px solid var(--chrome);
  box-shadow:var(--shadow-2);
  font-size:var(--fs-text-md);
  animation:toast-in var(--dur-2) var(--ease-out);
  transition:opacity var(--dur-2) var(--ease), translate var(--dur-2) var(--ease);
}
.toast.is-out{ opacity:0; translate:0 calc(var(--s-3) * -1); }
@keyframes toast-in{ from{ opacity:0; translate:0 calc(var(--s-3) * -1); } }
.fx-lite .toast{ animation:none; }
@media (prefers-reduced-motion: reduce){ .toast{ animation:none; } }
.toast--ok   { border-left-color:var(--ok); }
.toast--amb  { border-left-color:var(--amb); }
.toast--error{ border-left-color:var(--red); }
.toast--error .fa{ color:var(--red); }

/* ==================================================================
   RAIL  —  the sections of one app, standing still
   ------------------------------------------------------------------
   The app bar says which APP you are in; this says which part of it,
   and unlike a menu it does not go away. The current section is a
   state, so it is the accent, marked on the LEADING EDGE — the same
   mark .menu__option already uses, because it is the same idea.
   `aria-current="page"` is what a screen reader reads; the class is
   only what the sheet paints.

   Below --bp-narrow it becomes a strip that scrolls sideways and the
   marker moves to the bottom edge, because a leading edge in a row
   points at nothing.
   ================================================================== */
.rail{ display:flex; flex-direction:column; gap:var(--s-4); min-width:0; }
.rail__nav{ display:flex; flex-direction:column; padding:var(--s-1) 0; }
.rail__nav .menu__option{ font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-sm); }
.rail__nav .menu__option[aria-current="page"]{
  color:var(--acc);
  background-color:rgba(var(--acc-rgb),.06);
  border-left-color:var(--acc);
  font-weight:600;
}
@media (hover: none){
  .rail__nav .menu__option:hover:not([aria-current="page"]){
    color:var(--text-dim); background:none; border-left-color:transparent;
  }
}

/* ==================================================================
   THE DOOR  —  the sign-in window
   ------------------------------------------------------------------
   The chapter in NEBULA.md says how it BEHAVES; this is what it looks
   like, so the next app does not redraw it. One card on the app's own
   backdrop, because signing in is a door in the same building and not
   a lobby someone else built. Everything here is an object that
   already exists — .site-bg, .btn--primary, the input reset, the
   .brand-text lockup — so it costs the language almost nothing new.

   It is --glass, not --overlay: it does not float over anything, it
   IS the page. And it has no entrance: this screen covers no wait,
   and the field under it is meant to be typed into on the first frame.
   ================================================================== */
.login-body{
  min-height:100dvh;
  display:grid; place-items:center;
  padding:var(--s-5);
}
.login{ width:100%; max-width:var(--door-w); }
.login__card{
  display:flex; flex-direction:column; gap:var(--s-3);
  background-color:var(--glass);
  backdrop-filter:var(--glass-blur);
  -webkit-backdrop-filter:var(--glass-blur);
  border:var(--hair) solid var(--line);
  border-radius:var(--radius);
  padding:var(--s-6) var(--s-5) var(--s-5);
  /* The exit, declared on the RESTING card so it runs the same way every
     time: on success the card leaves under its own power and the
     navigation waits for it, which turns a document swap into a hand-off
     instead of a white flash. */
  transition:opacity var(--boot-fade) var(--ease), scale var(--boot-fade) var(--ease-out);
}
.login__card.is-out{ opacity:0; scale:.97; pointer-events:none; }
@media (prefers-reduced-motion: reduce){
  /* keep the fade, drop the movement */
  .login__card.is-out{ scale:1; }
}
.login__mark{ display:flex; align-items:center; gap:var(--s-3); margin-bottom:var(--s-4); }
/* the lockup stacks here too: .nav__brand is the only other place that
   holds one, and the door is not the app bar */
.login__mark .brand-text{ display:flex; flex-direction:column; line-height:1.05; min-width:0; }
.login__mark .brand-name{
  font-family:var(--display-font);
  font-size:calc(17px * var(--display-scale));
  letter-spacing:var(--tr-display); text-transform:uppercase; color:var(--text);
}
.login__mark .brand-sub{
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:color-mix(in srgb, var(--acc-soft) 30%, var(--acc));
}
/* the version is a measured value, and it is data: not cased with the
   label it sits in */
.login__mark .brand-sub b{ font-weight:400; letter-spacing:var(--tr-num); color:var(--label); text-transform:none; }

/* a heading in the READING voice — never the display face (rule 4) */
.login__title{ margin:0; font-family:var(--sans); font-size:var(--fs-text-lg); font-weight:700; }
.login__note{ margin:0 0 var(--s-3); color:var(--text-dim); font-size:var(--fs-text-sm); }

/* Why you are looking at this page, when the app knows. The accent carries
   it as a fill and a rule; the words stay --text, because the accent never
   sits on its own tint (rule 1). NOT role="alert" — it answers a question,
   it does not interrupt. */
.login__why{
  margin:0 0 var(--s-3);
  padding:var(--s-3) var(--s-4);
  border-left:2px solid var(--acc);
  background-color:rgba(var(--acc-rgb),.08);
  color:var(--text);
  font-size:var(--fs-text-sm);
}
.login__label{
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-md); text-transform:uppercase;
  color:var(--label);
}
.login__field{ position:relative; }
.login__field input{
  /* 16px is the threshold under which iOS Safari zooms the page on focus,
     leaving the layout scrolled sideways with no way back. A BEHAVIOUR, not
     a type size: it does not come off the scale (rule 5's one exception). */
  font-size:16px;
  padding-block:var(--s-3);
}
/* a one-time code is read back digit by digit */
.login__code{ font-family:var(--mono); letter-spacing:var(--tr-otp); text-align:center; }

/* The reveal, inside the field. In the tab order on purpose: a reveal you
   can only reach with a mouse is no use to the person most likely to need
   it. The field keeps room for it. */
.login__reveal{
  position:absolute; top:50%; right:var(--s-3);
  translate:0 -50%;
  display:inline-flex; align-items:center; gap:var(--s-2);
  background:none; border:0;
  padding:var(--s-1) var(--s-2);
  cursor:pointer;
  font-family:var(--mono);
  font-size:var(--fs-chrome-xs); letter-spacing:var(--tr-chrome-xs); text-transform:uppercase;
  color:var(--text-ghost);
  transition:color var(--dur-1) var(--ease);
}
@media (hover: hover){ .login__reveal:hover{ color:var(--chrome); } }
.login__reveal:focus-visible{ color:var(--acc); }

/* Amber, not red: Caps Lock is not a refusal, it is the reason the refusal
   is about to happen. */
.login__caps{
  margin:0;
  color:var(--amb);
  font-family:var(--mono);
  font-size:var(--fs-chrome-sm); letter-spacing:var(--tr-chrome-md); text-transform:uppercase;
}
/* The one red message on the door. The glyph is what still says "refused"
   without the red, and both appear by `hidden` toggling — a line that
   animates in is a line you read late. */
.login__error{
  display:flex; align-items:baseline; gap:var(--s-2);
  margin:0;
  color:var(--red);
  font-size:var(--fs-text-sm);
}
.login__error > .fa{ flex:none; }
.login__submit{ margin-top:var(--s-3); min-height:var(--tap); }
.login__foot{
  margin:var(--s-4) 0 0;
  padding-top:var(--s-4);
  border-top:var(--hair) solid var(--line);
  color:var(--text-ghost);
  font-size:var(--fs-text-sm);
  line-height:1.9;
}
.login__foot code{ font-family:var(--mono); color:var(--text-dim); }

/* ==================================================================
   THE NARROW SCREEN
   ------------------------------------------------------------------
   The language owns one breakpoint. It used to own none: every
   responsive decision lived in showcase.css, the file the README
   calls "not part of the language" — so the app bar's behaviour
   under 760px was scaffolding a third app could not inherit, and
   every app rediscovered it.

   Below --bp-narrow: the bar sheds its middle, controls grow to a
   real touch target, and the type stops shrinking.
   ================================================================== */
@media (max-width: 760px){
  :root{
    /* 44px is the smallest target that is reliably hit with a thumb;
       under it, the tap that misses is read as a scroll. */
    --tap: 44px;
  }
  /* The search field is the bar's middle and the first thing to go:
     at this width it cannot be both full width and beside a brand.
     An app that needs search on a phone promotes it to its own row
     BELOW the bar — it does not shrink it into the corner. */
  .nav__search{ display:none; }
  .nav__inner{ gap:var(--s-4); justify-content:space-between; }

  .btn, .seg__opt, .tag{ min-height:var(--tap); padding-inline:var(--s-4); }
  .btn--sm{ min-height:36px; }
  .menu__option{ min-height:var(--tap); }
  .menu{ min-width:0; width:100%; }

  /* The rail becomes a strip that scrolls sideways, and the marker moves
     to the bottom edge: a leading edge in a row points at nothing. */
  .rail__nav{ flex-direction:row; overflow-x:auto; padding:0; scrollbar-width:none; }
  .rail__nav::-webkit-scrollbar{ display:none; }
  .rail__nav .menu__option{ width:auto; flex:0 0 auto; border-left:0; border-bottom:2px solid transparent; }
  .rail__nav .menu__option[aria-current="page"]{ border-bottom-color:var(--acc); }

  /* the controls drop to their own line rather than squeezing the prose */
  .set{ grid-template-columns:auto minmax(0, 1fr); }
  .set__aside{ grid-column:1 / -1; justify-content:flex-start; }
  .set__ico{ align-self:start; }

  .toasts{ left:var(--page-pad); align-items:stretch; }
  /* a static tag is a label, not a target: it does not take --tap */
  .tag--static{ min-height:0; padding:var(--s-1) var(--s-3); }

  .kv{ grid-template-columns:1fr; gap:var(--s-0) var(--s-4); }
  .kv dt{ color:var(--text-dim); }
  .kv dd{ margin-bottom:var(--s-3); }

  .hud__corner{ width:18px; height:18px; }
  .hud__bar{ padding:var(--s-4); gap:var(--s-4); }
}

/* ==================================================================
   TOUCH AND MOTION
   ------------------------------------------------------------------
   A :hover that REPAINTS costs the first tap on a phone — the tap
   lands as a hover, the element repaints, and the visitor has to tap
   again. So every colour/border hover is gated behind
   (hover: hover), and touch keeps only the movement.
   ================================================================== */
@media (hover: none){
  .card:hover{ background-color:var(--glass); border-color:var(--line); translate:none; box-shadow:none; }
  .card:hover::before, .card:hover::after{ opacity:0; }
  .card:hover .card__img img{ scale:1; filter:saturate(.85) brightness(.9); }
  .card:hover .card__hover{ opacity:0; }
  .tag:hover{ color:var(--text-dim); border-color:var(--line); background-color:var(--glass); }
  .btn:hover:not(:disabled){ color:var(--text-dim); border-color:var(--line); background-color:var(--glass); }
  .btn--primary:hover:not(:disabled){ color:var(--text); border-color:rgba(var(--acc-rgb),.60); background-color:rgba(var(--acc-rgb),.16); }
  .menu__option:hover{ color:var(--text-dim); background:none; border-left-color:transparent; }
  .foot__links a:hover{ color:var(--text-dim); border-bottom-color:transparent; }
}
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{ animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; }
  html{ scroll-behavior:auto; }
}

/* ==================================================================
   UTILITIES, LAST
   ------------------------------------------------------------------
   These override components, and a class beats another class only by
   coming later — so they come last, after every component and every
   gate. A utility declared next to the base loses to the .tag it was
   meant to correct, silently, and that is exactly the bug these two
   were added to fix.
   ================================================================== */

/* Rule 4 says never text-transform DATA — a name, a path, an address, a
   host, an identifier. The catch is that data often lands inside furniture
   that IS cased: a .menu__head, a .tag, a .section__doc. This is the tool
   for that, and it is the whole of it. */
.u-nocase{ text-transform:none; }

/* Out of the way of the eye, still in the layout and still a field a
   password manager can see — display:none is what makes managers ignore it,
   and what makes a screen reader skip a label that is doing real work. */
.u-offscreen{
  position:absolute;
  /* not a size off the scale: a 1px box is how an element stays in the
     accessibility tree and in a password manager's reach while taking no
     room. It is absolute, so it needs no negative margin to stay out of
     the layout either. */
  width:1px; height:1px;
  padding:0; border:0;
  overflow:hidden; white-space:nowrap;
  clip-path:inset(50%);
}
