/* ============================================================
   design-kit/glass.css — the liquid-glass material layer
   Built 2026-07-25. OPT-IN: link it after components.css, or
   don't link it at all. Nothing in the kit changes until a
   .dk-glass class is actually applied to something.

   WHAT THIS IS
   Apple's Liquid Glass, ported honestly to CSS. Apple describes
   the material as three named layers, and so does this file:
     Illumination = backdrop blur+saturate+brightness, + the tint
     Highlight    = the 1px specular rim (::before) + the sheen
     Shadow       = the inner depth band + the cast shadow
   Apple's optical model is LENSING, not scattering — which is why
   naive glassmorphism (blur + white box) never reads as iOS 26.
   We can't lens on the open web (SVG displacement in
   backdrop-filter is Chromium-only), so the read comes from the
   rim, the saturate, and the scroll-edge effect instead.

   WHERE IT GOES — this is doctrine, not taste.
   Apple HIG, Materials, verbatim: "Don't use Liquid Glass in the
   content layer." Glass is the floating navigation/control layer.
     YES → sticky header, tab/segmented bars, floating pills,
            toasts, modals, sheets, command palette, FAB, and the
            :active state of an in-content control.
     NO  → cards, tables, forms, code, prose, list rows, media
            frames. Those stay flat: panel + 1px --line + radius.
   That is AGENT-GUIDE.md hard rule #4 unchanged. Rule #4 already
   carves out "shadows ONLY on overlays/FAB" — glass is the same
   exemption, expressed as translucency instead of a drop shadow.
   See AGENT-GUIDE.md hard rule #4 for the full reconciliation.

   PERF BUDGET — hard ceiling, not a suggestion.
   Max 3 composited glass surfaces per viewport, of which AT MOST
   ONE may be persistently visible while the page scrolls.
   Mozilla bug 1731965 measured 24ms GPU/frame with a single
   backdrop-filter during gentle scroll vs 2ms without — one
   surface already blows the 16.7ms/60fps budget. Apple's own
   guidance (GlassEffectContainer) is to COMBINE glass into one
   container rather than instantiate many. Put the blur on the
   tray, never on the tokens inside it.

   SELF-CONTAINED
   Consumes exactly one host token, --accent, and only if it
   exists (falls back to folio mint). Everything else — edge,
   tint, alpha, radius, depth — is defined here as --glass-*.
   Deliberately does NOT touch --panel/--txt/--dim/--line:
   --line is opaque #262b33 in :root but rgba(255,255,255,.08)
   in folio, and --line2/--faint don't exist in every theme.
   This file therefore drops into design-kit OR straight into
   szczleon-portfolio's inline <style> unchanged.
   ============================================================ */


/* ============================================================
   1 · TOKENS
   ============================================================ */

/* @property first. THE trap this defends against, verified in
   Chromium 148: a var() that fails to resolve inside
   backdrop-filter is Invalid At Computed-Value Time — it does
   NOT fall back to an earlier declaration, it resets the whole
   property to `none`. A misspelled token would therefore give
   you a fully transparent panel with unreadable text on top,
   in exactly the case you thought you had covered.
   Registering the primitives means a bad token falls back to
   initial-value instead. Engines without @property ignore this
   block and get the plain :root defaults below — belt+braces. */
@property --glass-tint   { syntax: "<percentage>"; inherits: true; initial-value: 10%;  }
@property --glass-r      { syntax: "<length>";     inherits: true; initial-value: 16px; }
@property --glass-pad    { syntax: "<length>";     inherits: true; initial-value: 0px;  }
@property --glass-rim-hi { syntax: "<number>";     inherits: true; initial-value: 0.55; }
@property --glass-rim-lo { syntax: "<number>";     inherits: true; initial-value: 0.05; }
@property --glass-depth  { syntax: "<number>";     inherits: true; initial-value: 0.85; }
@property --glass-cast   { syntax: "<number>";     inherits: true; initial-value: 0.42; }
@property --glass-grain-o{ syntax: "<number>";     inherits: true; initial-value: 0;    }

:root {
  /* --- Raw material. Space-separated channels so alpha can be
         varied per-use with rgb(var(--x) / <a>) without redeclaring
         a colour. Never reference host text/panel vars here. --- */
  --glass-ink: 8 10 14;      /* near-black; the body of the glass */
  --glass-hi:  255 255 255;  /* the light */

  /* --- The accent hook. This is the ONLY host token consumed.
         The portfolio rewrites --accent on <html> per hiring lane
         and .bgfx crossfades over 600ms, so a color-mix tint
         re-themes for free and animates with it. Hard-coded rgba
         would desync four ways (the existing .pcard::after ring
         is the cautionary example — it never re-themes). --- */
  --glass-accent: var(--accent, #74e0c4);

  /* how much accent bleeds into the ink. 10% is the ceiling that
     keeps every lane inside the contrast numbers in §3. */
  --glass-tint: 10%;

  /* --glass-base / --glass-solid are ALSO re-declared on the glass
     classes themselves — see the note directly below §1. Do not
     delete either copy. */
  --glass-base: color-mix(in srgb, var(--glass-accent) var(--glass-tint), rgb(var(--glass-ink)));

  /* NOTE: fill alpha is NOT a token. Apple's size rule inverts the
     naive instinct — larger glass is MORE opaque, not airier — and
     small glass additionally flips light/dark against its backdrop
     in iOS, which CSS cannot do. Opacity is therefore doing the job
     the OS does live, and it is tuned per tier against measured
     contrast in §3 rather than left as a knob to guess at. */

  /* the no-backdrop-filter / reduced-transparency / high-contrast
     collapse target. 97% alpha = readable over anything. */
  --glass-solid: color-mix(in srgb, var(--glass-base) 97%, transparent);

  /* --- Blur scale. DOCUMENTED HERE, but MIRRORED AS LITERALS in
         the filter chains in §3. Two reasons the chain can't be
         var()-driven: (a) the IACVT trap above, (b) MDN BCD #25914
         reports Safari 18.3 rejecting custom properties inside
         -webkit-backdrop-filter entirely. Literals cost one line
         and remove both risks. IF YOU CHANGE THESE, CHANGE THE
         LITERALS IN §3 TOO — they are the ones that render. --- */
  --glass-blur-chrome: 24px;  /* default tier: bars, toasts, pills */
  --glass-blur-panel:  32px;  /* thick tier: modals, sheets, palette */
  --glass-blur-thin:   20px;  /* field tier: over a bounded dark page */
  --glass-blur-clear:   6px;  /* clear variant: over photo/video */
  --glass-sat:        150%;

  /* --- Highlight. Conic, so the light has an ANGULAR position:
         bright at top-left, dead at bottom-right, bounce at ~60%.
         A linear gradient can only do one axis and reads as a
         gradient border, not as a lit edge. --- */
  --glass-rim-hi: .55;
  --glass-rim-lo: .05;

  /* --- Shadow. Inner band = material thickness. Cast = separation.
         Apple scales both with size and makes them adaptive; we
         scale them per tier and leave it at that. --- */
  --glass-depth: .85;
  --glass-cast:  .42;

  /* --- Shape. Apple's default glass shape is a CAPSULE, because
         a half-height radius is self-concentric at any nesting
         depth. We default to 16px because kit chrome is mostly
         rectangular, and ship .dk-glass-capsule for the rest. --- */
  --glass-r: 16px;
  --glass-pad: 0px;           /* set alongside --glass-r for concentric children */

  /* --- Grain. Off by default: it costs a compositing layer and
         only earns its keep on large hero-scale surfaces. --- */
  --glass-grain-o: 0;

  /* the scroll-edge strip height (see §7) */
  --glass-edge-h: 44px;
  --glass-edge-top: 0px;
}

/* ---- RE-DECLARED ON THE ELEMENT. This is not redundancy. -------
   A custom property that contains var() is resolved WHERE IT IS
   DECLARED, and descendants then inherit the already-resolved
   value. So with --glass-base declared only at :root, overriding
   --glass-accent or --glass-tint further down the tree does
   NOTHING — the mix has already been computed against :root's
   accent. Verified in the harness: `.cvok{--glass-accent:var(--live)}`
   produced the unchanged mint tint, and `.dk-toast.ok/.bad` were
   silently dead for the same reason.
   Re-declaring the two derived tokens on the glass classes makes
   them resolve against THAT element's accent and tint, so
   per-component retinting works — while the :root copies above
   stay as the page-wide default and keep --glass-solid available
   to descendant rules (the banned-elements block in §6). */
.dk-glass,
.dk-glass-faux {
  --glass-base:  color-mix(in srgb, var(--glass-accent) var(--glass-tint), rgb(var(--glass-ink)));
  --glass-solid: color-mix(in srgb, var(--glass-base) 97%, transparent);
}


/* ============================================================
   2 · THE MATERIAL — base rule
   Everything here is engine-agnostic and renders identically
   with or without backdrop-filter. The fallback background lives
   HERE, in the base rule, so a non-supporting browser keeps a
   readable panel; §3 only upgrades it. Never invert this into
   `@supports not (...)` — a mis-evaluated condition would then
   leave you with a transparent box and no text contrast.
   ============================================================ */
/* POSITIONING — :where() on purpose, and this is load-bearing.
   The rim/grain pseudos need a positioned ancestor, so the glass
   element must not be static. But a naive `.dk-glass{position:relative}`
   has the SAME specificity as `.dk-header{position:sticky}` /
   `.dk-toast{position:fixed}` in components.css and, because glass.css
   loads later, silently CLOBBERS them — the sticky header stops
   sticking and the toast/FAB/overlay stop being fixed. Verified in
   the harness: the header vanished on scroll.
   :where() is specificity ZERO, so it applies only when the element
   has no position of its own, and every real component keeps
   whatever components.css (or the host page's `nav {}`) declared. */
:where(.dk-glass, .dk-glass-faux) { position: relative; }

.dk-glass,
.dk-glass-faux {
  /* own stacking context: keeps the rim/grain/sheen pseudos from
     escaping, and stops a parent's blend modes reaching in.
     isolation does NOT create a backdrop root — verified. */
  isolation: isolate;
  border-radius: var(--glass-r);
  /* the rim REPLACES the 1px border. Apple: hierarchy comes from
     layout and grouping, not from added backgrounds and borders. */
  border: 0;

  /* SOLID FALLBACK. Overridden in §3 where blur is available. */
  background-color: var(--glass-solid);

  box-shadow:
    /* SHADOW · inner depth band. Big negative spread pulls it into
       a tight strip at the bottom = the look of material thickness. */
    inset 0 -18px 26px -24px rgb(var(--glass-ink) / var(--glass-depth)),
    /* SHADOW · faint internal bounce so the middle isn't dead flat */
    inset 0 0 22px 0 rgb(var(--glass-hi) / .05),
    /* SHADOW · cast. Rule #4 restricts shadows to overlays/FAB —
       glass IS only ever chrome/overlay, so this stays inside it. */
    0 14px 38px -12px rgb(0 0 0 / var(--glass-cast));

  /* Only cheap, compositor-friendly properties. NEVER put
     backdrop-filter in a transition: it re-rasterises the whole
     backdrop every frame, and Chromium issue 40175472 documents
     the blur vanishing outright mid-transition. */
  transition: background-color .22s ease, box-shadow .22s ease;
}

/* HIGHLIGHT · the 1px specular rim.
   Built as a padding ring double-masked with mask-composite.
   Two mask layers — the full box and the content box — XORed
   leave exactly the 1px padding ring, and because the pseudo
   inherits border-radius the ring tracks the curve perfectly at
   any radius including 50% circles and 999px capsules.
   Why not the alternatives:
     border-image  → ignores border-radius entirely.
     padding-box/border-box two-background trick → the border-box
       layer paints UNDER the padding-box layer, so it cannot
       coexist with a translucent fill. Fatal for glass.
   This is already the house idiom: szczleon-portfolio's
   .pcard::after hover ring uses the same XOR. */
.dk-glass::before,
.dk-glass-faux::before {
  content: "";
  position: absolute;
  inset: 0;
  box-sizing: border-box;      /* explicit: inset:0 + padding needs it */
  border-radius: inherit;
  padding: 1px;                /* = rim thickness */
  pointer-events: none;
  z-index: 2;                  /* a 1px ring over the padding edge —
                                  never lands on text */
  background-image: conic-gradient(from 200deg at 50% 50%,
    rgb(var(--glass-hi) / var(--glass-rim-hi)),
    rgb(var(--glass-hi) / .06) 20%,
    rgb(var(--glass-hi) / 0)   40%,
    rgb(var(--glass-hi) / .28) 58%,   /* bounce light off the surface below */
    rgb(var(--glass-hi) / var(--glass-rim-lo)) 76%,
    rgb(var(--glass-hi) / var(--glass-rim-hi)));
  -webkit-mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
          mask-image: linear-gradient(#000 0 0), linear-gradient(#000 0 0);
  -webkit-mask-clip: content-box, border-box;
          mask-clip: content-box, border-box;
  -webkit-mask-composite: xor;      /* WebKit spelling */
          mask-composite: exclude;  /* standard spelling — both required */
  transition: background-image .22s ease;
}

/* SHADOW/texture · optional grain. z-index:-1 puts it above the
   fill and below content, so it textures the material without
   sitting on the type. Off unless .dk-glass-grain is applied. */
.dk-glass::after,
.dk-glass-faux::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: -1;
  opacity: var(--glass-grain-o);
  mix-blend-mode: overlay;
  /* feTurbulence fractalNoise, inline. No external request.
     baseFrequency .85 = UI-scale grain (not film grain).
     stitchTiles='stitch' is MANDATORY or the 160px tile seams show.
     feColorMatrix saturate 0 kills turbulence's coloured speckle. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 160px 160px;
}
.dk-glass-grain { --glass-grain-o: .16; }  /* >.20 reads as JPEG artifacting */


/* ============================================================
   3 · ILLUMINATION — the backdrop tiers
   THE FILTER ORDER IS LOAD-BEARING: blur() → saturate() → brightness().
     blur()       averages neighbouring pixels, which mathematically
                  DESATURATES the backdrop (averaging drifts toward
                  grey). This is why DIY glassmorphism looks like
                  muddy frosted plastic.
     saturate()   AFTER the blur restores the chroma the blur
                  destroyed. Before the blur it does nothing useful.
     brightness()  LAST, and it is the entire accessibility story.
                  blur() is a convex combination — it preserves the
                  local mean and CANNOT raise contrast; over a white
                  photo, blurred white is still white. brightness(k)
                  is multiplicative, so it BOUNDS any backdrop at k
                  regardless of content. That bound is what makes
                  the tier auditable: there is a WORST case, and it
                  is the one quoted per tier below, so text that
                  passes there passes at every scroll offset.
                  It BOUNDS contrast — it does not equalise it.
                  (Measured on szczleon.com: the same nav text runs
                  ~8:1 over the hero photo and ~15:1 over the dark
                  field. Design for the floor, never the average.)
                  It must be last, because saturate() can push
                  channels back up after it.

   Values below are literals on purpose (see the note in §1).
   Contrast is computed against the worst possible backdrop —
   pure white — with #e8eaef text, in every lane.
   ============================================================ */
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {

  /* ---- DEFAULT · chrome tier ------------------------------------
     The safe default for anything that can travel over unknown
     content: toasts, floating pills, sticky bars, the FAB.
     ⚠ Re-measured 2026-07-25 — the figures previously here dropped
     the 10% accent tint (see .dk-glass-bar). Over a WHITE backdrop:
       worst-case composited surface ≈rgb(101,105,106)
       #e8eaef 4.59:1 ✅ body — but that clears AA by 0.09, not by
                half a point. Do not dim the text or raise the fill
                on this tier without re-measuring.
       #ffffff 5.52:1 ✅
       --dim/--muted #9aa1ad 2.12:1 ❌ — BANNED on this tier.      */
  .dk-glass {
    background-color: color-mix(in srgb, var(--glass-base) 12%, transparent);
    -webkit-backdrop-filter: blur(24px) saturate(150%) brightness(.50);
            backdrop-filter: blur(24px) saturate(150%) brightness(.50);
  }

  /* ---- .dk-glass-thin · FIELD tier ------------------------------
     Cheapest and most refractive — no brightness clamp, so the
     backdrop reads through almost undamaged.
     ⚠ VALID ONLY over a backdrop that is bounded BY DESIGN: the
     page's own dark field (.dk-bgfx + .dk-gridfield, §8). Over
     that the worst composite is ≈rgb(42,50,75) and #e8eaef is
     ~12:1 — even --dim clears 5.5:1. Over a white photo the
     identical tier is 1.4:1. Never let this tier scroll over
     media or user content.                                        */
  .dk-glass-thin {
    --glass-depth: .60;
    --glass-cast: .30;
    background-color: color-mix(in srgb, var(--glass-base) 10%, transparent);
    -webkit-backdrop-filter: blur(20px) saturate(160%);
            backdrop-filter: blur(20px) saturate(160%);
  }

  /* ---- .dk-glass-bar · DARKEST chrome tier ----------------------
     For navigation bars, which are full of 13-14px secondary links
     the default tier cannot carry.

     ⚠ NUMBERS CORRECTED 2026-07-25 — the figures previously here
     (rgb(53,53,53) / 4.69:1) were derived from --glass-ink ALONE
     and dropped the 10% accent tint, which lifts G/B. Re-measured
     against the shipped fill .66 / brightness(.55):
       worst-case surface (over WHITE) ≈rgb(60,68,69)
       #e8eaef  8.25:1 ✅ body
       #9aa1ad  3.82:1 ❌ FAILS AA — do NOT put --dim/--muted here
                unless the backdrop is bounded below ~mid-grey
                (4.5:1 from there down; 7.05:1 over the page's own
                dark field). Over media or a photo, use --txt.

     And the clamp BOUNDS the worst case — it does not equalise it.
     Contrast still swings ~8:1 → 15:1 across a scroll. Design for
     the floor, not the average.

     Still LESS opaque than the 72%-black bar it replaces on
     szczleon.com, because the clamp does work the alpha used to.  */
  .dk-glass-bar {
    /* still the densest tier, because it carries the smallest text —
       but 66% was a wall. 22% + a harder clamp reads as glass and
       measures better for white than 66% did for #e8eaef. */
    background-color: color-mix(in srgb, var(--glass-base) 22%, transparent);
    -webkit-backdrop-filter: blur(24px) saturate(150%) brightness(.44);
            backdrop-filter: blur(24px) saturate(150%) brightness(.44);
  }

  /* ---- .dk-glass-thick · PANEL tier -----------------------------
     Modals, sheets, sidebars, the command palette. Apple: larger
     surfaces are MORE opaque, and glass grows more opaque still
     as engagement deepens (a half sheet dragged to full height).
     ⚠ Re-measured 2026-07-25: worst-case surface ≈rgb(71,76,77).
       #e8eaef 7.22:1 ✅  ·  #ffffff 8.69:1 ✅
       #9aa1ad 3.34:1 ⚠ large text / UI only — never body.        */
  .dk-glass-thick {
    --glass-depth: 1;
    --glass-cast: .58;
    --glass-rim-hi: .65;
    background-color: color-mix(in srgb, var(--glass-base) 18%, transparent);
    -webkit-backdrop-filter: blur(32px) saturate(140%) brightness(.42);
            backdrop-filter: blur(32px) saturate(140%) brightness(.42);
  }

  /* ---- .dk-glass-clear · Apple's SECOND VARIANT -----------------
     Apple ships exactly two variants, Regular and Clear, and says
     they must NEVER be mixed in one interface. Everything above is
     Regular. Clear is highly translucent with NO adaptive
     behaviour, for controls floating over photo/video.
     Apple gates it on three simultaneous conditions:
       1. it sits over media-rich content,
       2. the content tolerates a dimming layer,
       3. the labels on it are BOLD and BRIGHT.
     Because it has no adaptivity it REQUIRES that dim — Apple
     specifies 35% dark. We bake the dim into the background stack
     as its own layer so the two can never be separated by a
     careless author, and we add a brightness(.75) clamp Apple
     doesn't need (the OS samples the backdrop live; CSS can't).
     With the clamp: #ffffff ≈4.96:1 ✅ · #e8eaef ≈4.09:1 ⚠ large.
     Use white 600+ weight, ≥17px. Nothing else.                   */
  .dk-glass-clear {
    --glass-rim-hi: .90;      /* on media the rim is the only edge cue */
    --glass-depth: .40;
    --glass-cast: .34;
    background-color: transparent;
    background-image:
      linear-gradient(rgb(var(--glass-hi) / .12), rgb(var(--glass-hi) / .12)),
      linear-gradient(rgb(0 0 0 / .35), rgb(0 0 0 / .35));   /* the mandatory dim */
    -webkit-backdrop-filter: blur(6px) saturate(180%) brightness(.75);
            backdrop-filter: blur(6px) saturate(180%) brightness(.75);
  }
  /* keep the grain layer working when .dk-glass-clear owns background-image */
  .dk-glass-clear.dk-glass-grain::after { mix-blend-mode: soft-light; }
}


/* ============================================================
   4 · .dk-glass-faux — the no-blur escape hatch
   Same rim, same depth, same tint, ZERO GPU cost, and it works
   in places where a real backdrop-filter is physically dead:
     · inside any ancestor that is a BACKDROP ROOT (opacity<1,
       filter, mask, clip-path, mix-blend-mode, isolation… no:
       isolation is safe, but the rest are not),
     · over an opaque scrim, where there is nothing to sample,
     · on elements small enough (<32px tall) that a 20px blur
       mostly samples its own edge and reads flat anyway,
     · anywhere you are already at the 3-surface perf budget.
   Layered gradients fake the top-lit falloff that the backdrop
   would otherwise supply. Use it freely — it is not rationed. */
.dk-glass-faux {
  --glass-depth: .55;
  --glass-cast: .28;
  background-color: color-mix(in srgb, var(--glass-base) 34%, transparent);
  background-image: linear-gradient(
    180deg,
    rgb(var(--glass-hi) / .085) 0%,
    rgb(var(--glass-hi) / .022) 42%,
    rgb(var(--glass-hi) / 0)    72%,
    rgb(var(--glass-hi) / .035) 100%);   /* bottom bounce */
}
/* faux over a genuinely dark, bounded field can afford to be airier */
.dk-glass-faux.dk-glass-thin {
  background-color: color-mix(in srgb, var(--glass-base) 20%, transparent);
}


/* ============================================================
   5 · SHAPE + NESTING
   ============================================================ */

/* Apple's default glass shape. A capsule's radius is always half
   its height, so it is self-concentric at any nesting depth and
   never needs recomputing — which is exactly why the system
   defaults to it. Use for pills, statusbars, floating tabs. */
.dk-glass-capsule { --glass-r: 999px; }

/* CONCENTRIC CORNERS, as Apple's formula:
   inner radius = outer radius − padding (WWDC25 s356).
   max(0px, …) reproduces Apple's documented edge case — a child
   far from the container corner resolves to SQUARE, which is
   correct, not a bug. calc() can't read a parent's computed
   radius, so declare --glass-r and --glass-pad together on the
   container and children inherit both. */
.dk-glass > .dk-glass-inner,
.dk-glass-faux > .dk-glass-inner {
  border-radius: max(0px, calc(var(--glass-r) - var(--glass-pad)));
}
/* standalone fallback: the component keeps its own capsule when
   it is NOT nested (Apple's "concentric shape + fallback radius") */
.dk-glass-inner { border-radius: 999px; }

/* ---- NEVER GLASS ON GLASS ------------------------------------
   WWDC25 s219, categorical: "always avoid glass on glass."
   Stacking the material makes the interface read as clutter, and
   Apple's positive instruction is to use FILLS, TRANSPARENCY and
   VIBRANCY for the top element so it feels like a thin overlay
   that is PART OF the material.
   It is also physically pointless: an element with backdrop-filter
   is itself a backdrop root, so a nested blur samples only the
   parent's own paint, never the page — a guaranteed flat result
   at full GPU price.
   Enforced structurally, because a later author WILL re-nest it. */
.dk-glass :is(.dk-glass, .dk-glass-faux, .dk-glass-thin, .dk-glass-thick,
              .dk-glass-bar, .dk-glass-clear) {
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  background-color: rgb(var(--glass-hi) / .07);
  background-image: none;
  box-shadow: none;
}
.dk-glass :is(.dk-glass, .dk-glass-faux)::before { opacity: .45; }  /* thinner rim inside */

/* Any element with a backdrop-filter also creates a CONTAINING
   BLOCK for abs/fixed descendants — verified: a position:fixed
   child with top:0/left:0 lands at the host's box, not the
   viewport. So modals, dropdowns, tooltips and command palettes
   must be portalled to <body>, never rendered inside glass chrome. */


/* ============================================================
   6 · INTERACTION
   Apple's model: THE LIGHT MOVES, THE PANEL DOESN'T. The material
   "illuminates from within" on touch; it does not change shape and
   it does not recolour its border.
   This is the ONE place AGENT-GUIDE rule #4's "state = recolor the
   border" bends — because glass has no border to recolour; the rim
   IS the border, and Apple brightens it instead. Geometry never
   changes, so the spirit of the rule survives.
   ============================================================ */
.dk-glass-hover { cursor: pointer; }

/* hover/focus: fill lifts, rim catches more light. Both are cheap
   compositor-side changes. backdrop-filter is deliberately NOT
   touched — animating blur re-rasterises the backdrop every frame,
   drops mid-range phones to ~15fps, and the resulting scroll jank
   is itself a vestibular trigger. */
.dk-glass-hover:is(:hover, :focus-visible, :focus-within) {
  --glass-rim-hi: .95;
  --glass-rim-lo: .12;
  background-color: color-mix(in srgb, var(--glass-base) 42%, transparent);
}
.dk-glass-thin.dk-glass-hover:is(:hover, :focus-visible, :focus-within) {
  background-color: color-mix(in srgb, var(--glass-base) 30%, transparent);
}
.dk-glass-bar.dk-glass-hover:is(:hover, :focus-visible, :focus-within) {
  background-color: color-mix(in srgb, var(--glass-base) 72%, transparent);
}

/* press: the panel SINKS toward the surface. Not a scale — Apple
   changes the light and the elevation, never the geometry. */
.dk-glass-hover:active { --glass-cast: .22; --glass-rim-hi: .70; }

/* ---- the sheen sweep -------------------------------------------
   Needs a real child, not a pseudo: ::before is the rim and
   ::after is the grain, and a component that collides with either
   is a bug you find in production.
   Markup:  <div class="dk-glass dk-glass-hover"><i class="dk-glass-sheen"></i> … </div>
   z-index:-1 keeps the band under the text — a .55-alpha white
   band sweeping ACROSS type is a legibility failure, not a style. */
.dk-glass-sheen {
  position: absolute;
  inset: -40%;
  z-index: -1;
  pointer-events: none;
  background-image: linear-gradient(115deg,
    transparent 38%,
    rgb(var(--glass-hi) / .14) 50%,
    transparent 62%);
  transform: translateX(-70%);
  opacity: 0;
  transition: transform .7s cubic-bezier(.32,.72,0,1), opacity .3s ease;
}
.dk-glass-hover:is(:hover, :focus-visible, :focus-within) .dk-glass-sheen {
  transform: translateX(70%);
  opacity: 1;
}

/* Marketing-hero only. NEVER on app chrome — an idle animation on
   a persistently-visible blurred surface is the worst combination
   of GPU cost and distraction in this whole file. */
@keyframes dkGlassSweep { to { transform: translateX(70%); } }
.dk-glass-sheen-auto { opacity: 1; animation: dkGlassSweep 2.8s cubic-bezier(.45,0,.55,1) infinite; }

/* ---- focus ----------------------------------------------------
   A single-colour ring cannot satisfy SC 1.4.11 / 2.4.13 on glass,
   because the ring is judged against BOTH the glass surface and
   the unbounded content one pixel outside it. Measured on the
   chrome tier: a white ring is 6.9:1 on the glass but 1.0:1 on a
   white photo; a black halo is 19.8:1 on white but 2.9:1 on the
   glass. Neither passes alone — together one always wins. */
.dk-glass :focus-visible,
.dk-glass-faux :focus-visible,
.dk-glass:focus-visible,
.dk-glass-faux:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgb(0 0 0 / .85);   /* restored under forced-colors in §9 */
}

/* ---- text law on glass ----------------------------------------
   Glass has exactly ONE text colour. Measured on the chrome tier:
     #ffffff 6.1:1 ✅ · #e8eaef 5.0:1 ✅ · #d2d2d2 4.5:1 (the floor)
     #9aa1ad 2.2:1 ❌ · #7a828e 1.7:1 ❌
     accent as TEXT: 2.1–3.7:1 depending on lane — fails in three
     of the portfolio's four lanes, so it can never be validated once.
   The lightest passing grey is visually indistinguishable from
   primary, so a "dim tier on glass" is a fiction. Express hierarchy
   with size, weight and letterspacing. Accent survives only as a
   non-text element — a rim, a dot, an underline — at 3:1.
   :where() = zero specificity, so any deliberate override wins. */
.dk-glass :where(p, li, span, small, td, dd, figcaption, .sub, .dim, .muted, .faint) {
  /* §13: with the fills cut for transparency, #e8eaef no longer clears
     4.5:1 over a white backdrop at any setting that buys real
     see-through. White does. This is §6's "exactly ONE text colour"
     with the value pinned rather than merely recommended. */
  color: #fff;
  /* opacity on text over DARK glass blends the glyph TOWARD the
     dark surface — it LOWERS contrast. It is a bug, not a style. */
  opacity: 1;
}
.dk-glass :where(a) {
  color: #fff;
  text-decoration-color: var(--glass-accent);   /* accent as a 3:1 non-text cue */
  text-underline-offset: 3px;
}
/* Grayscale antialiasing renders thinner than subpixel and eats
   the contrast margin a translucent surface doesn't have to spare.
   Opt-in, because switching it mid-page is visible on 1x monitors. */
.dk-glass-crisp { -webkit-font-smoothing: auto; }

/* HARD BAN — these never sit on glass. Long-form prose (NN/g: if
   the page's purpose is detailed written information, glass is not
   its background), dense/tabular data (comparing two cells means
   comparing two different backdrops), code and mono (thin strokes),
   and FORM INPUTS (SC 1.4.11 needs a 3:1 field boundary a
   translucent rim cannot guarantee against unknown content, and
   translucency collapses filled/empty and value/placeholder).
   Correct modal pattern: OPAQUE body + glass HEADER. */
.dk-glass :where(table, input, select, textarea, pre, code, article, .prose) {
  background-color: var(--glass-solid);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}


/* ============================================================
   7 · SCROLL EDGE EFFECT
   The single thing that makes web glass read as Apple glass.
   The common web pattern — "nav gains a solid background on
   scroll" — is the opposite of what iOS does. Apple keeps the
   MATERIAL constant and dissolves the CONTENT passing underneath,
   blurring it and reducing its opacity so the bar visually floats
   above a moving plane instead of clipping it.

   ⚠ IT CANNOT BE .dk-header::after. The header has a
   backdrop-filter, which makes it a backdrop root for its own
   descendants — a pseudo-element hanging below the header would
   sample only the header's paint (nothing) and render dead.
   The strip must therefore be a BODY-LEVEL element, positioned
   fixed just under the bar:
       <div class="dk-scrolledge" aria-hidden="true"></div>
   as a direct child of <body>, before the header.

   ONE PER VIEW. Apple: scroll edge effects are NOT decorative,
   they clarify where UI and content meet, and must not be used
   where there are no floating UI elements. No section-boundary
   fades, no bottom-of-viewport fades, no card edge fades.
   ============================================================ */
.dk-scrolledge {
  position: fixed;
  left: 0; right: 0;
  top: calc(var(--glass-edge-top) + env(safe-area-inset-top));
  height: var(--glass-edge-h);
  z-index: 19;                  /* under .dk-header (20), over content */
  pointer-events: none;
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  /* soft style (Apple's default): a gradual fade, so there is no
     hard cut line where the blur stops. The hard style is macOS-only. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, transparent 100%);
}


/* ============================================================
   8 · BACKDROP PRIMITIVES
   Glass over a FLAT background is invisible — blurring flat black
   yields flat black, and you pay full GPU price for nothing.
   No kit theme ships a backdrop: .dk-base is a solid var(--bg),
   which is exactly why .dk-header's existing blur(10px) shows no
   frost in components.html.
   These are szczleon-portfolio's .bgfx and .grid-field, normalised.
   Put both at the top of <body>. They are FIXED SIBLINGS at
   negative z-index — never wrappers — so nothing they do can
   become a backdrop root for the glass above them.

   ⚠ THE PAINT-ORDER TRAP — measured 2026-08-08, four cases:
     no background on html or body   → .dk-bgfx visible ✅
     background on BODY only         → visible ✅
     background on HTML only         → visible ✅
     background on BOTH              → INVISIBLE ❌
   The root element's background is propagated to the canvas, which
   sits behind everything. When only one of html/body declares one,
   that one propagates and the negative-z-index layer shows through.
   Declare a background on BOTH and html's propagates while body's
   is left to paint as an ordinary element background — and ordinary
   backgrounds paint AFTER negative-z-index descendants, so body
   covers the field completely.
   `html, body { background: … }` is a reflex in most resets, so
   this is easy to hit and gives no error: the page looks fine, the
   glass just quietly has nothing to sample and renders as a flat
   translucent rectangle. Set the page background in exactly one
   place, or set it on .dk-bgfx itself as the rule below does.
   ============================================================ */
.dk-bgfx {
  position: fixed; inset: 0; z-index: -2; pointer-events: none;
  transition: background-image .6s ease;   /* re-theme crossfade */
  background-color: var(--bg, #07080a);
  background-image:
    radial-gradient(900px 500px at 78% -8%,
      color-mix(in srgb, var(--accent2, #6aa3ff) 12%, transparent), transparent 60%),
    radial-gradient(800px 520px at 8% 12%,
      color-mix(in srgb, var(--accent, #74e0c4) 10%, transparent), transparent 60%);
}
.dk-gridfield {
  position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: .5;
  background-image:
    linear-gradient(rgb(var(--glass-hi) / .08) 1px, transparent 1px),
    linear-gradient(90deg, rgb(var(--glass-hi) / .08) 1px, transparent 1px);
  background-size: 46px 46px;
  /* radial mask keeps the grid from tiling to the viewport edges */
  -webkit-mask-image: radial-gradient(circle at 50% 30%, #000 0%, transparent 78%);
          mask-image: radial-gradient(circle at 50% 30%, #000 0%, transparent 78%);
}


/* ============================================================
   9 · COMPONENT RECIPES
   RE-SKIN ONLY. Every rule below changes surface, never layout —
   sizes, paddings, positions and z-indexes all come from
   components.css and are left alone.
   Usage: add .dk-glass (+ a tier) alongside the existing class.
   ============================================================ */

/* ---- sticky header --------------------------------------------
   .dk-header carries 13-14px --dim nav links, so it needs the
   secondary-text-safe tier, not the default one. Keeps sticky,
   z-index 20, the safe-area padding and the bottom hairline. */
.dk-header.dk-glass {
  --glass-r: 0px;                        /* full-bleed bar: no corners */
  --glass-cast: 0;                       /* a hairline separates it, not a shadow */
  border-bottom: 1px solid rgb(var(--glass-hi) / .10);
}
.dk-header.dk-glass::before { content: none; }   /* no rim on a full-bleed bar */
/* nested chips inside the bar: fill + hairline, never a second blur */
.dk-header.dk-glass .dk-nav button {
  background: rgb(var(--glass-hi) / .06);
  border-color: rgb(var(--glass-hi) / .12);
}
.dk-header.dk-glass .dk-nav button.active {
  background: var(--accent); border-color: var(--accent); color: var(--on-accent, #05130f);
}

/* ---- modal ------------------------------------------------------
   PICK ONE — you cannot have both:
   (a) .dk-ov.dk-glass-scrim → the iOS sheet idiom, blur the whole
       page behind the scrim. The nested guard in §5 then correctly
       forces .dk-box opaque, because .dk-ov is now a backdrop root.
   (b) .dk-box.dk-glass.dk-glass-thick → a glass panel; drop the
       scrim alpha so there is something left to see through. */
.dk-ov.dk-glass-scrim {
  background: rgb(0 0 0 / .38);
  -webkit-backdrop-filter: blur(18px) saturate(120%) brightness(.62);
          backdrop-filter: blur(18px) saturate(120%) brightness(.62);
  /* dkFade animates the overlay's OPACITY 0→1; while opacity<1 the
     overlay is a backdrop root for its children, so a glass .dk-box
     inside would render flat and then visibly snap on at the end.
     Fade the box instead. */
  animation: none;
}
.dk-ov:has(> .dk-box.dk-glass) { background: rgb(0 0 0 / .34); animation: none; }
@keyframes dkGlassIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.dk-box.dk-glass {
  --glass-r: 16px;                      /* matches components.css */
  --glass-pad: 18px;                    /* = its padding → concentric children */
  border: 0;
  box-shadow:
    inset 0 -18px 26px -24px rgb(var(--glass-ink) / 1),
    inset 0 0 22px 0 rgb(var(--glass-hi) / .05),
    var(--shadow-overlay, 0 24px 60px -30px rgb(0 0 0 / .8));
  /* the element's own opacity is safe — it only roots its
     DESCENDANTS' backdrops, not its own filter. */
  animation: dkGlassIn .22s cubic-bezier(.32,.72,0,1);
}
/* Apple's zero-cost modal pattern: opaque body, glass header. */
.dk-box .dk-glass-head {
  margin: -18px -18px 12px; padding: 14px 18px;
  border-bottom: 1px solid rgb(var(--glass-hi) / .10);
}

/* ---- command palette --------------------------------------------
   Keeps max-width 560, radius 14, overflow:hidden, the shadow.
   .res is a SCROLL CONTAINER — never blur it, or the backdrop is
   resampled on every scroll frame inside an already-blurred panel. */
.dk-cmd.dk-glass {
  --glass-r: 14px;
  border: 0;
  box-shadow:
    inset 0 -18px 26px -24px rgb(var(--glass-ink) / 1),
    var(--shadow-overlay, 0 24px 60px -30px rgb(0 0 0 / .8));
}
.dk-cmd.dk-glass input { border-bottom-color: rgb(var(--glass-hi) / .12); }
.dk-cmd.dk-glass .res { -webkit-backdrop-filter: none; backdrop-filter: none; }
/* the default hover fill is an OPAQUE var(--panel2) — on glass that
   reads as a solid bar sliding across frosted glass. Translucent. */
.dk-cmd.dk-glass .it:hover,
.dk-cmd.dk-glass .it.sel { background: rgb(var(--glass-hi) / .09); }

/* ---- toast ------------------------------------------------------
   Transient and small, so it can afford the thick tier. Position,
   transform-centring and the show/hide transition are untouched. */
.dk-toast.dk-glass { --glass-r: 11px; --glass-cast: .5; border: 0; }
.dk-toast.dk-glass.ok  { --glass-accent: var(--accent2, #46d27a); }
.dk-toast.dk-glass.bad { --glass-accent: var(--bad, #ff5f57); }

/* ---- FAB --------------------------------------------------------
   The gradient IS the FAB's job, so full glass would erase it.
   Correct form: a glass ring around a gradient core. Keeps 52px,
   the fixed position, the safe-area offset and the :active scale. */
.dk-fab.dk-glass {
  --glass-r: 50%;
  --glass-rim-hi: 1;
  background-color: transparent;
  background-image:
    radial-gradient(circle at 50% 50%,
      var(--accent, #74e0c4) 0 58%, transparent 62%);   /* the core */
  color: var(--on-accent, #05130f);
}
/* the badge's 2px opaque var(--bg) ring instantly betrays glass */
.dk-fab.dk-glass .badge { border-color: rgb(var(--glass-hi) / .22); }

/* ---- pills + statusbar ------------------------------------------
   Both are ~22-30px tall. A 20px blur on a 28px object mostly
   samples its own edge and reads as a flat lighter pill for full
   GPU price — so these get FAUX glass, capsule shape, no filter.
   This is a deliberate downgrade, not an oversight. */
.dk-pill.dk-glass,
.dk-statusbar.dk-glass {
  --glass-r: 999px;
  --glass-cast: .18;
  --glass-depth: .40;
  border: 0;
  background-color: color-mix(in srgb, var(--glass-base) 28%, transparent);
  background-image: linear-gradient(180deg,
    rgb(var(--glass-hi) / .085), rgb(var(--glass-hi) / .015) 60%, rgb(var(--glass-hi) / .03));
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}
.dk-pill.dk-glass::before,
.dk-statusbar.dk-glass::before { padding: .5px; }   /* hairline rim at pill scale */

/* ---- segmented control ------------------------------------------
   The iOS liquid-glass signature: a translucent trough with a
   specular travelling lozenge. Apple's performance rule and the
   no-glass-on-glass rule agree — the material goes on the TRAY,
   never on the buttons. --glass-pad makes the lozenge concentric. */
.dk-seg.dk-glass {
  --glass-r: 12px;
  --glass-pad: 2px;                     /* = its padding, per components.css */
  border: 0;
  background-color: color-mix(in srgb, var(--glass-base) 30%, transparent);
  background-image: linear-gradient(180deg,
    rgb(var(--glass-hi) / .05), rgb(var(--glass-hi) / .01));
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}
.dk-seg.dk-glass button { border-radius: max(0px, calc(var(--glass-r) - var(--glass-pad))); }
.dk-seg.dk-glass button.active {
  background: var(--accent);
  box-shadow: 0 1px 2px rgb(0 0 0 / .35), inset 0 1px 0 rgb(var(--glass-hi) / .35);
}

/* explicit class equivalent of the :has() rule above, for engines
   without :has() and for authors who prefer to be told, not sniffed */
.dk-ov.dk-glass-ov { background: rgb(0 0 0 / .34); animation: none; }


/* ============================================================
   10 · GUARDS — none of these are optional
   The collapse chain is SUBTRACTIVE: glass is the default and
   each signal overrides it. Do NOT invert it into the additive
   `@media (prefers-reduced-transparency: no-preference){ .glass{…} }`
   pattern that Chrome's own launch post recommends — Safari does
   not know that feature, so the query never matches there and
   glass silently disappears on every Apple device.
   ============================================================ */

/* ---- 10a · no backdrop-filter ---------------------------------
   MANDATORY, not a graceful degrade. Without the filter there is
   no brightness clamp, and a 32% tint over white is 1.7:1 — an
   unreadable page, not a plainer one. The base rule in §2 already
   holds the solid fallback; this only strips decoration that
   assumed a backdrop, and covers hardware-acceleration-off.
   The `or` form is REQUIRED: Chromium does not support the
   -webkit- property AT ALL (verified — CSS.supports false, the
   declaration is rejected outright), and Safari <18 supports only
   the -webkit- one. Either alone excludes an engine. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .dk-glass {
    background-color: var(--glass-solid);
    background-image: none;
    border: 1px solid rgb(var(--glass-hi) / .12);
  }
  .dk-glass::after { content: none; }
  .dk-scrolledge { display: none; }
}

/* ---- 10b · reduced transparency -------------------------------
   Chrome/Edge 118+ only, ~73% global. WebKit DECLINED to
   implement it as a fingerprinting vector — which is the exact
   inverse of where the OS setting lives (macOS/iOS ▸ Accessibility
   ▸ Display ▸ Reduce Transparency). Hence 10c and 10d. */
@media (prefers-reduced-transparency: reduce) {
  :is(.dk-glass, .dk-glass-faux) {
    -webkit-backdrop-filter: none !important;
            backdrop-filter: none !important;
    background-color: var(--glass-solid) !important;
    background-image: none !important;
    border: 1px solid rgb(var(--glass-hi) / .20) !important;
    box-shadow: none !important;
  }
  :is(.dk-glass, .dk-glass-faux)::before,
  :is(.dk-glass, .dk-glass-faux)::after { content: none !important; }
  .dk-glass-sheen, .dk-scrolledge { display: none !important; }
  .dk-ov.dk-glass-scrim { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; background: rgb(0 0 0 / .8) !important; }
}

/* ---- 10c · increased contrast ---------------------------------
   THE SIGNAL THAT COVERS SAFARI. 93% global (Safari 14.1+, iOS
   14.5+, Chrome 96+, Firefox 101+). macOS "Increase Contrast" also
   puts the system into reduced transparency, so treating `more` as
   a transparency signal is correct, not a workaround — and a user
   asking for contrast should not be handed a 5:1 surface when a
   15:1 one is available.
   Note the border goes SOLID: a 12%-white hairline is ~1.3:1 and
   is invisible to precisely the person who asked for contrast.
   This also lands the component back on rule #4's flat grammar. */
@media (prefers-contrast: more) {
  :is(.dk-glass, .dk-glass-faux) {
    -webkit-backdrop-filter: none !important;
            backdrop-filter: none !important;
    background-color: var(--bg, #07080a) !important;
    background-image: none !important;
    border: 1px solid #fff !important;
    box-shadow: none !important;
  }
  :is(.dk-glass, .dk-glass-faux)::before,
  :is(.dk-glass, .dk-glass-faux)::after { content: none !important; }
  .dk-glass-sheen, .dk-scrolledge { display: none !important; }
  .dk-glass :where(a) { text-decoration-thickness: 2px; }
}
/* `less` means less contrast, not more glass. Softening a 5:1
   surface further is how you arrive at 3:1. Deliberately empty. */

/* ---- 10d · the user toggle ------------------------------------
   Given WebKit's non-support of 10b, this is the ONLY mechanism
   that reaches 100% of users. NN/g recommends it explicitly and
   Apple shipped a glass-intensity slider in iOS 27 after nine
   months of Liquid Glass legibility complaints.
   Wire a control to html[data-glass="off"] + localStorage, and set
   the attribute from an inline <script> in <head> so it does not
   flash. No JS ships in this file. */
html[data-glass="off"] :is(.dk-glass, .dk-glass-faux) {
  -webkit-backdrop-filter: none !important;
          backdrop-filter: none !important;
  background-color: var(--glass-solid) !important;
  background-image: none !important;
  border: 1px solid rgb(var(--glass-hi) / .20) !important;
  box-shadow: none !important;
}
html[data-glass="off"] :is(.dk-glass, .dk-glass-faux)::before,
html[data-glass="off"] :is(.dk-glass, .dk-glass-faux)::after { content: none !important; }
html[data-glass="off"] .dk-glass-sheen,
html[data-glass="off"] .dk-scrolledge { display: none !important; }

/* ---- 10e · forced colors (Windows High Contrast) ---------------
   WHAT THE UA DOES FOR YOU: remaps color / background-color
   (including rgba and transparent) / border-color / outline-color
   to system colours, forces box-shadow AND text-shadow to none,
   and forces background-image — i.e. EVERY GRADIENT — to none.
   WHAT IT DOES NOT DO: backdrop-filter, filter and opacity are not
   in the adjustment list. The blur keeps computing, keeps costing
   GPU, and keeps creating a containing block. Kill it yourself.
   Two consequences already designed around: the rim is a
   background-image and therefore vanishes here (fine, borders
   survive), and anything that MEANT something via box-shadow has
   to re-say it with an outline. */
@media (forced-colors: active) {
  :is(.dk-glass, .dk-glass-faux) {
    -webkit-backdrop-filter: none !important;
            backdrop-filter: none !important;
    background: Canvas !important;
    color: CanvasText !important;
    border: 1px solid ButtonBorder !important;
    box-shadow: none !important;
  }
  :is(.dk-glass, .dk-glass-faux)::before,
  :is(.dk-glass, .dk-glass-faux)::after { content: none !important; }
  .dk-glass-sheen, .dk-scrolledge { display: none !important; }
  /* the two-tone focus ring loses its halo (box-shadow is stripped);
     a system-coloured ring is guaranteed to contrast because the
     whole palette is now the user's */
  .dk-glass :focus-visible, .dk-glass:focus-visible,
  .dk-glass-faux :focus-visible, .dk-glass-faux:focus-visible {
    outline: 3px solid Highlight !important;
    outline-offset: 2px;
    box-shadow: none !important;
  }
  .dk-glass a { color: LinkText !important; text-decoration: underline; }
}
/* ⚠ NEVER `forced-color-adjust: none` to preserve the look. It also
   disables the UA text backplate — the thing keeping text legible
   over images in high contrast — producing a worse result than
   doing nothing. Surrender to Canvas/CanvasText/ButtonBorder. */

/* ---- 10f · reduced motion -------------------------------------
   Kill the sweep and every transition. Do NOT remove the blur:
   blur is a static visual property, not motion, and stripping it
   also strips the brightness clamp — you would trade away the
   contrast guarantee to satisfy a motion preference. */
@media (prefers-reduced-motion: reduce) {
  :is(.dk-glass, .dk-glass-faux),
  :is(.dk-glass, .dk-glass-faux)::before,
  .dk-glass-sheen, .dk-bgfx {
    transition: none !important;
    animation: none !important;
  }
  .dk-glass-sheen, .dk-glass-sheen-auto { display: none !important; }
  .dk-box.dk-glass { animation: none !important; }
}

/* ---- 10g · rounded-corner + sticky guards ----------------------
   Honest status of the folklore:
   · The classic WebKit bug (bugzilla 158807 — blur leaking square
     past a rounded corner) was RESOLVED in 2016. Do not cargo-cult
     `transform: translateZ(0)` for it; it buys a compositing layer
     and fixes nothing.
   · What IS still open is Mozilla 1803813: in Firefox, a parent
     with BOTH border-radius AND overflow != visible containing a
     position:sticky child kills that child's backdrop-filter
     entirely. It renders fine in Chrome, so it passes local review
     and fails only in Firefox.
   · Old iOS can still bleed the filtered backdrop a hair past the
     curve. clip-path re-cuts the exact rounded box and hard-stops
     it. Opt-in, because clip-path CLIPS — never put it on anything
     with an overflowing child (the FAB badge, dropdown menus). */
.dk-glass-clip { clip-path: inset(0 round var(--glass-r)); }
/* Firefox sticky fix: keep the rounded+overflow combination off the
   ANCESTOR of a sticky glass bar; clip the bar itself instead. */
.dk-glass-sticky { position: sticky; top: 0; border-radius: var(--glass-r); overflow: clip; }

/* ---- 10h · the ancestor contract ------------------------------
   NOT ENFORCEABLE IN CSS, and NOT DETECTABLE AT RUNTIME —
   getComputedStyle still reports blur(24px) on an element that is
   rendering completely unblurred, so this class of bug is invisible
   to tests and only catchable by eye. It has to be a convention.

   NEVER put any of these on an ANCESTOR of a glass surface:
     opacity < 1  ·  filter  ·  mask / mask-image / mask-border
     clip-path    ·  mix-blend-mode  ·  backdrop-filter
     will-change naming any of the above
   Each establishes a BACKDROP ROOT, which truncates what the glass
   can sample. The panel still paints — it just blurs nothing, so it
   degrades to a flat translucent rectangle rather than visibly
   breaking. `filter: saturate(1)` and `opacity: .999` are visual
   no-ops that still kill it.
   The classic trap: a scroll-reveal wrapper that animates opacity.
   Put the glass ON the revealing element, never inside it — or use
   .dk-glass-faux there, which is immune.

   SAFE, contrary to widespread advice: transform, will-change:
   transform, isolation: isolate, overflow: hidden. All verified
   non-breaking. */


/* ============================================================
   11 · LENSING — the refraction layer
   Added 2026-08-08. Needs glass-lens.js. The REFRACTION rules are
   gated on html[data-lens="on"], which only the script ever sets, so
   without it nothing refracts. The droplet shape preset in 11b, the
   guard re-assertions in 11c2/11dd and the reduced-motion belt in
   11d apply unconditionally, by design — they are the rules that have
   to hold when the script is absent or has failed.

   WHAT IT CLOSES
   The header of this file, lines 13-17, says we can't lens on the
   open web and settles for the rim, the saturate and the scroll
   edge. That was the right call for a cross-engine default and it
   is still what every other engine gets. It was NOT a reason to
   leave the effect unbuilt where it does work. Chromium executes
   SVG displacement inside backdrop-filter, so there it now gets
   real refraction: the backdrop is geometrically RESAMPLED, not
   merely averaged.

   Three behaviours, all falling out of one displacement map:
     EDGE REFRACTION  the backdrop bends where the surface curves.
     BACKGROUND MORPH straight lines come out curved; the middle
                      stays calm and the shoulder does the work.
     PRISM            three displacement passes at slightly
                      different scales, one per channel, so the rim
                      splits colour. Read section 11c before using
                      it — it is far more situational than it looks.

   USAGE
     <link rel="stylesheet" href="glass.css">
     <script src="glass-lens.js" defer></script>
     <div class="dk-glass dk-glass-lens"> … </div>

   .dk-glass-lens IS ONLY EVER HONOURED ALONGSIDE .dk-glass, and
   that pairing is doing real work, not tidiness. FOUR of section
   10's guards — 10b reduced-transparency, 10c prefers-contrast,
   10d data-glass="off", 10e forced-colors — set
   `backdrop-filter: none !important` on
   :is(.dk-glass, .dk-glass-faux). (10a is unreachable here: no
   backdrop-filter means no url() either. 10f is motion, handled by
   11d and by the script.) An !important author declaration
   outranks a normal inline declaration in the cascade, so the
   script writing an inline backdrop-filter cannot outlive
   reduced-transparency, prefers-contrast, data-glass="off" or
   forced-colors. Verified in the harness. A standalone
   .dk-glass-lens would have escaped all five, which is why the
   selector below requires both and the script's own selector does
   too.
   ============================================================ */


/* ---- 13 · THE TRANSPARENCY PASS — retuned 2026-08-08 -----------
   The tiers below were originally carrying their contrast in the
   FILL. That is backwards, and it costs the thing the material
   exists for: at 32% fill the surface is a milky sheet and you
   cannot see what is behind it, which is the whole point of glass.

   The fix is to move the work to the clamp, where §3 always said it
   belonged. brightness() bounds the backdrop no matter what is
   behind it; the fill only ever hid it. Measured over §3's worst
   case (pure white), tracking how much of the backdrop's own
   variation survives — (1 − fill) × brightness:

     fill .32  brightness .55   see 0.374   #fff 5.52  #e8eaef 4.59
     fill .20  brightness .55   see 0.440   #fff 4.54  #e8eaef 3.78
     fill .12  brightness .50   see 0.440   #fff 4.70  #e8eaef 3.90
     fill .10  brightness .50   see 0.450   #fff 4.57  #e8eaef 3.79

   So ~18% more of the picture comes through, and WHITE text is
   still above 4.5:1 — but #e8eaef no longer is, at any setting that
   buys real transparency. That is the trade, and it is not
   negotiable by tuning: it is the same ceiling §6 already found
   when it said glass has exactly ONE text colour.
   Hence §13a below: on glass, text is #fff. Not "prefer", is. */

/* ---- 11a · the lensed tiers ------------------------------------
   Displacement is destroyed by blur — the two are opposites, one
   moves pixels and the other averages them away, so a 24px blur
   spends GPU erasing exactly what the lens just computed. Lensed
   tiers therefore drop the blur hard and let the geometry carry
   the read.

   THE CONTRAST FLOOR SURVIVES THIS, and here is why, because it
   is the first thing that looks like it should break. Section 3's
   guarantee comes from brightness(), not from blur(): blur is a
   convex combination that preserves the local mean and cannot
   raise contrast, while brightness(k) is multiplicative and BOUNDS
   any backdrop at k. Displacement only permutes pixels — it
   creates no luminance — so the worst case over a white backdrop
   is still white, still clamped at k, and section 3's measured
   numbers hold verbatim at every blur radius. The literals below
   keep each tier's original clamp untouched for that reason.

   What DOES change is high-frequency noise under text, which is a
   legibility problem contrast maths does not describe. A lensed
   surface carrying body copy keeps a real blur (the default and
   bar tiers below); the near-zero radii are on the tiers Apple
   already gates to non-text or media use. */
@supports (backdrop-filter: blur(1px)) {
  /* No -webkit- twin anywhere in this section, deliberately.
     Chromium does not support the -webkit- property at all
     (section 10a), and Chromium is the only engine that reaches
     these rules, so a -webkit- copy would be pure noise. */
  html[data-lens="on"] .dk-glass.dk-glass-lens {
    backdrop-filter: blur(8px) saturate(150%) brightness(.55);
  }
  html[data-lens="on"] .dk-glass.dk-glass-bar.dk-glass-lens {
    backdrop-filter: blur(8px) saturate(150%) brightness(.55);
  }
  html[data-lens="on"] .dk-glass.dk-glass-thick.dk-glass-lens {
    backdrop-filter: blur(10px) saturate(140%) brightness(.45);
  }
  /* .dk-glass-thin gets NO reduction here, deliberately. 11a's whole
     safety argument is that section 3's floor lives in brightness() —
     and .dk-glass-thin is the one tier in this file with no
     brightness() at all. Its contrast comes instead from section 3's
     precondition that it only ever sits over the page's own bounded
     dark field. Lensing does not change that precondition, but it
     also cannot inherit an argument the tier was never part of, so
     the tier keeps its shipped blur and the lens simply reads softer
     on it. */
  /* Clear is the variant with edges left intact, so it is the one
     tier where dispersion has anything to disperse. */
  html[data-lens="on"] .dk-glass.dk-glass-clear.dk-glass-lens {
    backdrop-filter: blur(2px) saturate(180%) brightness(.75);
  }
}

/* ---- 11b · the droplet preset ----------------------------------
   The script reads the computed border-radius, so shape alone would
   need no configuration. The FILL does.

   A bead of water is a clear lens. The default tier's 32% fill and
   its blur exist to make chrome readable over unknown content, and
   both are exactly wrong here: they bury the magnification that is
   the entire point, and the result reads as a flat grey disc rather
   than as water. Measured — with the shipped chrome tier the three
   surface profiles were indistinguishable on screen, because the
   fill was doing all the work and the optics none of it.

   So this preset strips the fill to a trace and leaves the rim to
   describe the shape, which is what actually happens physically:
   you see a drop by its edge and by what it does to what is behind
   it, never by its body.

   ⚠ DECORATIVE ONLY. This is the one surface in the file with no
   contrast floor, because it has no text and nothing may be put on
   it. It is a showpiece for a hero, not a control. Anything that
   has to be READ belongs on a tier from section 3. */
.dk-glass-drop {
  /* NOT `--glass-r: 50%`. --glass-r is registered @property as <length>
     in section 1, so a percentage is Invalid At Computed-Value Time and
     silently resolves to the 16px initial value — which would then be
     handed to .dk-glass-clip's clip-path and to the concentric child
     formula in section 5, both of which would cut a 16px-rounded box out
     of a circle. This is the exact trap section 1 exists to describe, so
     the radius is declared directly and --glass-r is left alone. */
  --glass-rim-hi: .95;      /* a bead's rim is its brightest feature */
  --glass-depth: .30;
  --glass-cast: .34;
  border-radius: 50%;
  aspect-ratio: 1;
}
@supports (backdrop-filter: blur(1px)) {
  html[data-lens="on"] .dk-glass.dk-glass-drop.dk-glass-lens {
    /* 3%, not the 6% this shipped with. A bead of water has no body to
       see — it is read entirely by its rim and by what it does to what
       is behind it — and any fill above a trace starts competing with
       the magnification it is supposed to be showing off. Lower this
       further and the rim is doing all the work, which is honest but
       reads as a soap bubble rather than water. */
    background-color: color-mix(in srgb, var(--glass-base) 3%, transparent);
    background-image: none;
    /* barely any blur: displacement is the whole read, and blur is
       the one thing guaranteed to erase it */
    backdrop-filter: blur(.5px) saturate(150%);
  }
}

/* ---- 11c · PRISM, and when it is worth anything -----------------
   Opt in per element with data-lens-prism="0.05".

   MEASURED IN THE HARNESS, and this is the part that decides
   whether you should use it at all. The same panel, same prism
   setting, over four conditions:
     sharp backdrop, no blur    → fringe clearly visible
     sharp backdrop, blur(6)    → nearly gone
     sharp backdrop, blur(24)   → gone entirely
     the kit's own .dk-bgfx     → nothing, at any setting
   Dispersion is a function of the LOCAL BACKDROP GRADIENT. Where
   the backdrop has no edges there is nothing to split, and a real
   water bead over a flat wall shows no colour either. So prism
   needs a high-frequency backdrop AND a near-zero blur, which in
   practice means .dk-glass-clear over photo or video — exactly the
   surface Apple already gates to media-rich content.

   Corollary, stated plainly so nobody debugs a working build:
   over the kit's own gradient field prism renders NOTHING. That is
   correct behaviour, not a broken filter.

   HONEST RANGE, AND WHY THE HONEST NUMBER IS UNUSABLE HERE.
   The physically-real spread is the reciprocal Abbe number: water
   ≈ 1/55.7, so ca ≈ 0.009; dense flint reaches ≈ 0.039. The
   script's cap is 0.06, and past roughly that it stops reading as
   glass and starts reading as misregistered print.

   The visibility floor is a separate calculation, and it rules most
   of that range out. The red-to-blue split at the rim is
       split = 2·ca·scale·|C − ½|
   and |C − ½| maxes at 127/255 = 0.498, so the split is ca·scale —
   NOT 2·ca·scale. That factor-of-two slip is worth naming because
   an earlier draft of this section carried it and therefore
   recommended a value that renders nothing at all.
   The split has to clear roughly 1.5px before bilinear filtering
   eats it. At the default strength 14 the scale is 28, so a
   physically honest ca = 0.009 buys 0.25px — invisible, and no
   strength the fold clamp permits will rescue it.

   SO: ca ≥ 0.04, or don't turn it on. That is a deliberate
   departure from the physics, and the reason is that the physics
   assumes a real droplet's magnification, which a 150px UI surface
   does not have. The same arithmetic is why prism wants a
   physically LARGE surface: on a 44px pill there is no room for a
   shoulder wide enough to split visibly without folding.

   COST. Three displacement passes instead of one, each landing in
   its own render target: roughly 3-4x the single-pass lens (ESTIMATED from the pipeline shape, not measured).
   BUDGET: at most ONE prism surface per page, and it must not be
   persistently visible during scroll. Banned on .dk-header,
   .dk-glass-bar and .dk-scrolledge for that reason. The
   single-pass lens may take the one persistent slot from the perf
   budget in this file's header; prism may not. */

/* ---- 11d · reduced motion, as CSS rather than as a promise ------
   The script already tears the lens down on this signal, so these
   rules are the belt: they hold if the script fails, if it is never
   loaded, or if the preference flips between attach and teardown.

   Note what they restore — the SHIPPED tier from section 3, not
   `backdrop-filter: none`. Setting none would strip brightness()
   and trade section 3's contrast bound for a motion preference,
   which is precisely the trade section 10f refuses to make. And
   because displacement and blur are both identity over a constant
   backdrop, swapping the chain back changes the worst case by
   exactly zero: this costs nothing and RAISES the blur, which is
   more forgiving, not less.
   !important because the lens chain is a non-important inline
   style written by glass-lens.js. */
@media (prefers-reduced-motion: reduce) {
  /* NOTE THE MISSING .dk-glass, and do not "fix" it. These selectors are
     deliberately held at two classes, (0,2,0), so that 11dd's (0,2,1)
     re-assertion outranks EVERY one of them. Adding the co-class for
     consistency pushes the three tier rules to (0,3,0) — above 11dd —
     and quietly restores blur on a bar, thick or clear surface for a
     user who has reduced motion AND reduced transparency. That bug was
     shipped once already, caught by review, and it looked exactly like
     a tidiness improvement both times.
     The pairing gate belongs on 11a, which GRANTS a lower blur. These
     rules only ever RESTORE the safe tier, so applying them to an
     unpaired element is harmless by construction.
     !important is what makes them beat 11a regardless of specificity. */
  .dk-glass-lens       { backdrop-filter: blur(24px) saturate(150%) brightness(.55) !important; }
  .dk-glass-bar.dk-glass-lens   { backdrop-filter: blur(24px) saturate(150%) brightness(.55) !important; }
  .dk-glass-thick.dk-glass-lens { backdrop-filter: blur(32px) saturate(140%) brightness(.45) !important; }
  .dk-glass-clear.dk-glass-lens { backdrop-filter: blur(6px)  saturate(180%) brightness(.75) !important; }
}

/* ---- 11g · THE CARD EXCEPTION, and what it costs to take --------
   Requires all three: .dk-glass.dk-glass-lens.dk-glass-lens-card

   THIS CROSSES A LINE THIS FILE DRAWS TWICE, so it says so rather
   than pretending otherwise. Section 2's doctrine is categorical —
   "NO → cards, tables, forms, code, prose, list rows, media frames"
   — quoting Apple HIG's "don't use Liquid Glass in the content
   layer". And section 11b's droplet is decorative-only with no
   contrast floor at all. A card is content AND it carries text, so
   it is the intersection of both bans.

   What makes this shippable rather than reckless is that it does
   NOT reuse the droplet's 3% fill. It takes the PANEL tier
   wholesale — fill 46%, brightness(.45) — which section 3 measured
   against a worst-case white backdrop at:
       #e8eaef  7.22:1 ✅   ·   #ffffff 8.69:1 ✅
       #9aa1ad  3.34:1 ⚠ large text / UI only, never body
   Those numbers survive verbatim here, by section 11a's argument:
   the floor comes from brightness(), displacement only permutes
   pixels and creates no luminance, so the bound is unchanged at
   any blur radius. The lens is added on top of a surface that was
   already measured to carry body copy — it does not soften it.

   The dim tier stays banned (section 6's text law is unchanged),
   and inputs stay banned outright (section 6's hard ban, SC
   1.4.11) — a card with a form in it does not get this.

   RADIUS IS NOT DECORATION HERE. The shoulder is clamped to the
   corner radius, so radius is what buys bend: at the kit's default
   16px a card refracts almost nothing, and the script will refuse
   below its visibility floor. 30px is the smallest radius that
   still reads as a card rather than a pill and gives the lens
   something to work with.

   AND IT NEEDS SOMETHING BEHIND IT. A card sitting on a flat page
   colour refracts nothing, for full GPU price — the same lesson as
   prism in 11c. Use it only over a real backdrop: the kit's
   .dk-gridfield, a photo, or video. Over flat --bg, don't. */
.dk-glass-lens-card {
  --glass-r: 30px;
  --glass-pad: 22px;          /* = its padding, so children stay concentric */
  --glass-depth: .9;
  --glass-cast: .5;
  border-radius: 30px;
}
@supports (backdrop-filter: blur(1px)) {
  html[data-lens="on"] .dk-glass.dk-glass-lens-card.dk-glass-lens {
    background-color: color-mix(in srgb, var(--glass-base) 18%, transparent);
    backdrop-filter: blur(10px) saturate(140%) brightness(.42);
  }
}
/* Section 6's bans are re-stated at this specificity because 11g
   out-ranks them and they are the reason this exception is safe. */
.dk-glass-lens-card :where(input, select, textarea, table, pre, code) {
  background-color: var(--glass-solid);
  backdrop-filter: none;
}

/* ---- 11c2 · the surfaces that stay flat ------------------------
   Section 11's selectors are three classes plus an attribute, which
   out-ranks two sets of deliberate `backdrop-filter: none`
   declarations elsewhere in this file — both non-important and both
   at (0,2,0):
     · section 5's glass-on-glass kill. Nesting is not merely
       discouraged there, it is physically pointless: the outer
       surface is itself a backdrop root, so the inner one samples
       its parent's paint and never the page.
     · section 9's pills, statusbar and segmented tray, which were
       downgraded on purpose because a 20px blur on a 28px object
       mostly samples its own edge for full GPU price.
   Adding .dk-glass-lens to any of them would have quietly revived a
   filter those sections spent paragraphs removing, so the kill is
   re-stated at a specificity that survives section 11. */
/* The html[data-lens] prefix is not decoration — it is the specificity.
   11a's tier rules run to (0,4,1); a bare `.dk-glass :is(…).dk-glass-lens`
   is only (0,3,0) and would LOSE to them, re-enabling the very filter
   this block exists to remove. Matching the prefix ties at (0,4,1), and
   this block is declared after 11a, so it wins on order. */
html[data-lens="on"] .dk-glass :is(.dk-glass, .dk-glass-faux).dk-glass-lens,
html[data-lens="on"] .dk-pill.dk-glass-lens,
html[data-lens="on"] .dk-statusbar.dk-glass-lens,
html[data-lens="on"] .dk-seg.dk-glass-lens {
  backdrop-filter: none !important;
}

/* ---- 11dd · THE GUARDS, RE-ASSERTED ABOVE 11d ------------------
   This block exists to fix a regression that 11d itself introduced,
   and the arithmetic is worth writing down because it is easy to
   repeat.

   Section 10's guards are `:is(.dk-glass, .dk-glass-faux)` — one
   class, specificity (0,1,0), with !important. Section 11d's
   reduced-motion belt is `.dk-glass.dk-glass-lens` — two classes,
   (0,2,0), also !important. Between two !important author
   declarations specificity decides, so 11d WINS.
   The consequence is exactly backwards: a user who has reduced
   motion set AND reduced transparency (or prefers-contrast: more,
   or forced-colors) would get 11d's blur chain put back on a
   surface that section 10 had deliberately collapsed to an opaque
   panel. The narrower preference would override the stronger one.

   Rather than edit section 10 — which is shipped, audited, and not
   this layer's to churn — the four guards that null the filter are
   re-stated here for lensed elements only, at (0,2,1). The bare
   `html` prefix is what buys the extra element token, and that is
   the whole trick: it clears 11d's (0,2,0) and nothing else moves.
   ============================================================ */
@media (prefers-reduced-transparency: reduce) {
  html :is(.dk-glass, .dk-glass-faux).dk-glass-lens { backdrop-filter: none !important; }
}
@media (prefers-contrast: more) {
  html :is(.dk-glass, .dk-glass-faux).dk-glass-lens { backdrop-filter: none !important; }
}
@media (forced-colors: active) {
  html :is(.dk-glass, .dk-glass-faux).dk-glass-lens { backdrop-filter: none !important; }
}
html[data-glass="off"] :is(.dk-glass, .dk-glass-faux).dk-glass-lens {
  backdrop-filter: none !important;
}

/* ---- 11e · the two author overrides -----------------------------
   html[data-glass-lens="off"]  drops the refraction, keeps the
     glass. Sibling to section 10d's data-glass="off", one level
     finer. NO CSS IS NEEDED FOR IT and none is written: the script
     reads the attribute, never attaches, and the element simply
     keeps the tier section 3 already gave it. A rule here would
     have to re-state a tier chain and would then be a second copy
     of section 3's literals to keep in sync — exactly the
     duplication section 1 warns about.
   html[data-glass-lens="on"]   forces the lens on past the engine
     gate. It exists because that gate has a live upstream clock on
     it: WebKit has an implementation landed but not shipped, and
     the day it ships the sniff is wrong in the conservative
     direction. This is the no-code-change escape, and the hook for
     a QA rig. */

/* ---- 11f · what the script exposes ------------------------------
   Per-element data attributes, all optional:
     data-lens-profile="puck|drop"   puck is a bevelled slab, all
                                     the action at the rim; drop is
                                     a sessile droplet, peaking in
                                     an annulus. Default puck.
     data-lens-strength="14"         peak inward displacement in px.
                                     CLAMPED — see below.
     data-lens-edge="40"             shoulder width in px.
     data-lens-prism="0.05"          0 = off. Below ~0.04
                                     renders nothing — see 11c.
     data-lens-ior="1.5"             1.5 crown glass, 1.333 water.
     data-lens-theta="62"            surface tilt at the rim, deg.
   And it writes back:
     data-lens-applied="on|clamped"  clamped means strength was
                                     reduced to keep the field
                                     renderable.
     html[data-lens]="on|reduced|unsupported"

   THE FOLD CLAMP. A displacement field whose Jacobian exceeds 1
   is no longer injective — neighbouring output pixels pull from
   inverted input positions and the backdrop folds back over
   itself. Measured on a 300x230 panel, shoulder 46px, cubic
   falloff:  J 0.52 clean · J 0.98 clean and near the material's
   limit · J 1.70 corner hooks · J 2.61 a solid smear bar across
   the bottom edge. The script computes the profile's peak slope
   and clamps strength to whatever still renders, so asking for
   more than the geometry supports gets you the maximum rather
   than a glitch. Widen the shoulder if you want more bend.

   REDUCED MOTION. The script strips the lens under
   prefers-reduced-motion, which is deliberately NOT what section
   10f does for blur. The reasoning differs because the situation
   does: blur is a static property, but a displacement field fixed
   to the element has the backdrop sliding THROUGH it, so content
   warps and unwarps as the page scrolls. That is optical flow,
   which is the class of motion the preference exists to suppress. */


/* ============================================================
   12 · INK — the press that dyes the water
   Added 2026-08-08. Needs glass-ink.js. Without it the class is
   inert and the button is ordinary glass.

   WHAT IT IS
   Press a glass control and a drop of colour is injected at the
   contact point, spreads through the material with a ragged,
   feathered front, and settles as a TINT — the glass ends up that
   colour and stays translucent. It is not a Material ripple: a
   ripple is a clean expanding circle that fades to nothing and
   leaves no trace, and it reads as plastic.

   THE TWO THINGS THAT MAKE IT READ AS INK RATHER THAN A RIPPLE
   1. The front is TORN, not circular. A radial blob is pushed
      through feTurbulence + feDisplacementMap, so its edge grows
      tendrils instead of staying a arc. Same primitive pair the
      lens uses in section 11, driven by noise instead of an SDF.
   2. It LANDS. When the spread finishes the blob is retired and
      its colour is written into --glass-accent on the element
      itself. Section 1 already re-declares --glass-base and
      --glass-solid on the glass classes precisely so a per-element
      accent re-mixes, so the material genuinely becomes that
      colour rather than wearing a coloured overlay. Translucency,
      rim, blur and the lens all survive untouched.

   COST: one extra compositing layer per button while the spread
   runs, and the turbulence is rasterised ONCE per press, not per
   frame — only the blob's transform animates, which is
   compositor-side. Do not animate baseFrequency; it re-rasters the
   whole filter region every frame.
   ============================================================ */

.dk-glass-ink { position: relative; overflow: hidden; isolation: isolate; }

/* z-index -1 is glass.css's established slot for a layer that sits
   ABOVE the fill and BELOW the content — same as the grain in §2.
   The label must never be inked over. */
.dk-glass-ink > .dk-ink {
  position: absolute;
  z-index: -1;
  pointer-events: none;
  /* Layers ADD rather than cover. Two dyes in front of a backlight
     add light, so where a new press overlaps an older one you get
     the genuine mix of the two hues instead of the newer one simply
     hiding the older. This is the whole reason a press makes a NEW
     blob rather than recolouring the existing one. */
  mix-blend-mode: plus-lighter;
  left: var(--ink-x, 50%);
  top:  var(--ink-y, 50%);
  /* 280% covered any button completely — which meant every torn edge
     fell outside the box and all you ever saw was the flat middle of
     the gradient. The cloud has to be only a little larger than what
     it is filling, so its structure lands INSIDE the visible area.

     NOT aspect-ratio:1. Forcing the blob square sizes it off the
     element's WIDTH in both axes, so on a wide, shallow control it is
     enormous in the direction nobody can see: the lane trough is about
     600x50, and a square blob at 165% is 990x990 — roughly TWELVE
     TIMES the pixels, every one of them pushed through a 4-octave
     turbulence, three times over. Tracking both axes gives 990x82 and
     the same picture. The blob is an ellipse on a wide control, which
     is also what a drop spreading in a shallow channel does.

     The multiplier itself is per-element (--ink-size, written by
     glass-ink.js) because ONE number cannot work at every size, and
     getting it wrong makes the filter free of charge in the worst
     sense — it still rasterises, it just has nothing visible to do.
     The torn boundary sits at (blobRadius x scale) from the centre;
     if that lands outside the element's own edge, every pixel you can
     actually see is flat interior gradient. Measured at 165% flat:
       lane button 100x36   edge inside until scale 1.60  ✅
       trough      524x46   edge inside until scale 1.38  ✅
       panel      1000x300  edge inside until scale 0.72  ✗ (rest is 1.18)
     So a big surface was paying for a filter whose signature never
     reached the glass. The script now solves for the multiplier
     instead of assuming one. */
  width:  calc(var(--ink-size, 1.65) * 100%);
  height: calc(var(--ink-size, 1.65) * 100%);
  translate: -50% -50%;
  transform: scale(0);
  opacity: 0;
  /* No will-change. The running transform animation is itself a direct
     compositing reason for the ~260ms it lasts, and after that nothing
     here needs its own layer — but will-change would stop the
     compositor ever folding these back. On a -hold control the blobs
     are never removed, so that is 1.5MB of render surface per lobe
     retained for the life of the page. */
}

/* ---- 12b · the cloud ------------------------------------------
   A single turbulence-displaced circle has ONE torn silhouette, and
   a silhouette that only scales reads as a balloon inflating. Real
   diffusion keeps re-forming: the boundary is always rearranging
   even when the front has stopped advancing.

   Three lobes, each with its OWN turbulence seed, each rotating at a
   different rate and in alternating directions. Rotating a displaced
   blob does not spin a picture — the gradient underneath is radially
   symmetric, so there is nothing to see rotating. What moves is the
   TENDRILS, because they are the noise field sweeping across a
   symmetric source. That is the whole trick, and it is why this
   costs three transforms rather than an animated filter: nothing
   re-rasterises, the compositor just turns three already-rendered
   layers.
   Never animate baseFrequency to get this. It re-rasters the entire
   filter region every frame and is the single most expensive thing
   in this file. */
.dk-glass-ink > .dk-ink > i {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  /* closest-side keeps the stops tied to the lobe, not its box */
  /* Denser falloff than a plain blob: the mid stops are what read as
     depth inside the cloud once the lobes overlap each other. A long
     flat plateau just gives you a coloured disc. */
  background: radial-gradient(circle closest-side,
    rgb(var(--ink, 116 224 196) / .52) 0 14%,
    rgb(var(--ink, 116 224 196) / .40) 34%,
    rgb(var(--ink, 116 224 196) / .22) 54%,
    rgb(var(--ink, 116 224 196) / .08) 72%,
    transparent 86%);
}
.dk-glass-ink > .dk-ink > i:nth-child(1) {
  filter: url(#dkInkTurbA);
  animation: dkInkRollA 19s linear infinite;
}
/* the two inner lobes are deliberately SMALLER and off-centre, so
   their torn boundaries fall inside the surface rather than beyond
   it — that is what makes the interior look like weather instead of
   like paint */
.dk-glass-ink > .dk-ink > i:nth-child(2) {
  inset: 15% 21% 19% 13%;
  filter: url(#dkInkTurbB);
  animation: dkInkRollB 27s linear infinite;
  opacity: .85;
}
.dk-glass-ink > .dk-ink > i:nth-child(3) {
  inset: 30% 16% 24% 30%;
  filter: url(#dkInkTurbC);
  animation: dkInkRollC 34s linear infinite;
  opacity: .7;
}
/* the scale wobble is what stops the three reading as one rigid body */
@keyframes dkInkRollA { from{transform:rotate(0)   scale(1)   } 50%{transform:rotate(180deg) scale(1.06)} to{transform:rotate(360deg) scale(1)} }
@keyframes dkInkRollB { from{transform:rotate(360deg) scale(1.04)} 50%{transform:rotate(180deg) scale(.95)} to{transform:rotate(0) scale(1.04)} }
@keyframes dkInkRollC { from{transform:rotate(0)   scale(.97) } 50%{transform:rotate(-180deg) scale(1.08)} to{transform:rotate(-360deg) scale(.97)} }
/* THE STATE THE BLOB IS IN IS THE BLOB'S OWN BUSINESS.
   These used to key off .dk-inking / .dk-inked on the ELEMENT, which
   welded the colour to the animation: the surface could not look
   selected until the spread had finished. Measured on a 40px lane
   button: 848ms from pointerdown to the committed colour, against a
   ~100ms threshold for a press reading as instantaneous. The colour
   is INFORMATION and the cloud is DECORATION, so they are now two
   independent state machines — .dk-inked lands on the element in the
   same frame as the press, while .dk-ink-go travels here. */
.dk-glass-ink > .dk-ink.dk-ink-go {
  transform: scale(1);
  opacity: 1;
  /* slow-out, so the front decelerates the way a diffusion front does
     rather than arriving at constant speed */
  /* --ink-dur is written per element by glass-ink.js from the box's
     own diagonal: a 40px control and a 300px panel should not share a
     duration, because the front has to cross very different distances
     to read as the same speed. */
  transition: transform var(--ink-dur, 360ms) cubic-bezier(.16,.84,.28,1),
              opacity 140ms ease-out;
}
/* THE HAND-OFF. The blob does NOT fade to nothing — ink that
   vanishes is a ripple, and the brief was that the colour stays in
   the water. It settles to a low-alpha wash that still covers the
   surface, and because the wash is a radial gradient anchored at
   the contact point it stays slightly stronger where the drop
   landed, which is what real ink does.
   The --glass-accent commit in glass-ink.js runs alongside it, so
   the rim, the specular and the depth band all pick up the colour
   too — the wash alone would only tint the face. */
/* an older layer keeps whatever opacity the script diluted it to */
.dk-glass-ink > .dk-ink.dk-ink-settled {
  transform: scale(1.35);
  transition: transform .6s ease-out, opacity .6s ease-out;
}
.dk-glass-ink > .dk-ink.dk-ink-rest {
  transform: scale(1.18);
  opacity: .45;
  transition: transform .5s ease-out, opacity .35s ease-out;
}
/* Once a layer has settled its lobes stop turning. The billow is what
   sells the TRAVEL; a settled dye does not keep churning, and leaving
   12+ filtered layers rotating forever is steady-state GPU spent on
   something nobody is looking at. Paused rather than removed, so a
   re-press resumes from where it was instead of snapping. */
.dk-glass-ink > .dk-ink.dk-ink-rest > i,
.dk-glass-ink > .dk-ink.dk-ink-settled > i { animation-play-state: paused; }
/* And the material's own tint comes up with it. §1 caps the default
   at 10% because that is the ceiling that keeps every LANE inside
   §3's contrast numbers on a surface carrying 13-14px secondary
   text. An inked control is a different case: it is a deliberate
   selected state, its label is bold white, and the contrast that
   matters is measured below rather than inherited. */
.dk-glass-ink.dk-inked {
  --glass-tint: 20%;
  /* AND the clamp comes DOWN, which is the part that is easy to skip.
     A settled wash of mid-tone colour LIGHTENS the surface, so the
     inherited brightness(.55) no longer holds. Measured over §3's
     worst case — a pure white backdrop — across the four kit accents:
         wash .55 / tint .20 / brightness .55  →  3.32:1  ✗
         wash .45 / tint .18 / brightness .45  →  4.30:1  ✗
         wash .45 / tint .20 / brightness .34  →  5.17:1  ✓
     The shipped row is the third. White, bold, is the only label
     colour this state is measured for; #e8eaef and anything dimmer
     are not, and §6's ban on --dim stands. */
  --glass-brightness-inked: .34;
}
@supports (backdrop-filter: blur(1px)) {
  .dk-glass.dk-glass-ink.dk-inked {
    backdrop-filter: blur(24px) saturate(150%) brightness(.34);
  }
  html[data-lens="on"] .dk-glass.dk-glass-ink.dk-inked.dk-glass-lens {
    backdrop-filter: blur(8px) saturate(150%) brightness(.34);
  }
}

/* ---- 12a · guards ----------------------------------------------
   Reduced motion gets the RESULT without the travel — the colour is
   the information, the spreading is the decoration. Nothing here
   removes the tint, because the tint is state.
   The other four section-10 signals need no work: they strip the
   backdrop-filter, and the ink is an ordinary background layer that
   keeps working on the collapsed opaque surface. */
@media (prefers-reduced-motion: reduce) {
  /* Skip the TRAVEL, keep the RESULT. An earlier version zeroed the
     blob here, which was right when the blob was a transient ripple
     and wrong the moment it became the thing carrying the colour:
     under reduced motion the selected control lost its only
     indicator entirely. The colour is information; the spreading is
     decoration, and only the decoration goes. */
  .dk-glass-ink > .dk-ink { transition: none !important; }
  .dk-glass-ink > .dk-ink.dk-ink-go,
  .dk-glass-ink > .dk-ink.dk-ink-rest {
    transform: scale(1.18) !important;
    opacity: .45 !important;
  }
}
@media (forced-colors: active) {
  .dk-glass-ink > .dk-ink { display: none !important; }
}


/* ---- 12c · HOVER — the surface wets under the pointer -----------
   A glass control should respond to a pointer that has not committed
   yet. §6's existing hover lifts the fill and brightens the rim,
   which is correct but tells you nothing about WHERE the pointer is.
   This adds a soft bloom that tracks it, so the material reads as
   something the cursor is touching rather than a region it is inside.

   It is a TRANSFORM-TRANSLATED layer, not a moving background
   position or a re-drawn gradient: those repaint the element on
   every pointermove, which on a surface that already owns a
   backdrop-filter means re-running the blur too. The bloom is one
   composited layer that the compositor slides.

   glass-ink.js writes --hx/--hy in px, already rAF-coalesced. */
.dk-glass-ink > .dk-bloom {
  position: absolute;
  z-index: -1;
  left: 0; top: 0;
  width: 46%;
  aspect-ratio: 1;
  margin: -23% 0 0 -23%;
  border-radius: 50%;
  pointer-events: none;
  mix-blend-mode: plus-lighter;
  background: radial-gradient(circle closest-side,
    rgb(var(--glass-hi) / .16) 0 30%,
    rgb(var(--glass-hi) / .07) 58%,
    transparent 78%);
  transform: translate3d(var(--hx, 0px), var(--hy, 0px), 0) scale(.6);
  opacity: 0;
  transition: opacity .26s ease, transform .1s linear;
}
.dk-glass-ink:hover > .dk-bloom,
.dk-glass-ink:focus-visible > .dk-bloom {
  opacity: 1;
  transform: translate3d(var(--hx, 0px), var(--hy, 0px), 0) scale(1);
}
/* on an already-inked control the bloom picks up the dye instead of
   staying white — the water it is disturbing is coloured now */
.dk-glass-ink.dk-inked > .dk-bloom {
  background: radial-gradient(circle closest-side,
    rgb(var(--ink, 116 224 196) / .30) 0 30%,
    rgb(var(--ink, 116 224 196) / .12) 58%,
    transparent 78%);
}
@media (prefers-reduced-motion: reduce) {
  /* the bloom is pointer-driven motion, so it goes entirely; the
     hover state is still carried by §6's fill and rim lift */
  .dk-glass-ink > .dk-bloom { display: none !important; }
  .dk-glass-ink > .dk-ink > i { animation: none !important; }
}

/* ---- 12d · CLEARING — the water goes clear again ---------------
   Ink in water does not stay put; it keeps diffusing until the tint
   is gone. So by default a pressed control CLEARS: the layers fade
   out over --ink-clear (default 9s) and the committed accent is
   dropped, returning the surface to plain glass.

   .dk-glass-ink-hold opts out, and a TOGGLE MUST USE IT. This is the
   one place the two behaviours genuinely conflict: if the ink is
   carrying a selected state, clearing it deletes the only indicator
   that state has. Selection is not a transient response to a press,
   so it does not get a transient treatment. */
.dk-glass-ink.dk-ink-clearing > .dk-ink {
  opacity: 0 !important;
  transform: scale(1.5);
  transition: opacity var(--ink-clear, 9s) cubic-bezier(.3,0,.7,1),
              transform var(--ink-clear, 9s) linear;
}


/* ============================================================
   14 · MOTION — the Apple behaviours, and the one that cannot
   be done the obvious way
   Added 2026-08-08.

   THE RULE THAT SHAPES ALL OF THIS, restated because it decides
   every technique below: backdrop-filter may NEVER be transitioned
   or animated. Chromium re-rasterises the whole backdrop every
   frame and issue 40175472 has the blur vanishing outright
   mid-transition. So none of these animate the material. They
   animate GEOMETRY and LIGHT, which is also what Apple animates —
   WWDC25 s219 is explicit that on press the light moves and the
   panel does not.
   ============================================================ */

/* ---- 14a · GEL-IN — glass forms, it does not fade -------------
   Apple never cross-fades a glass surface into existence; it grows
   into place and the light catches up. A plain opacity fade reads
   as a PNG appearing, because a fading translucent panel is
   indistinguishable from a fading opaque one.

   Everything here is transform + opacity, both compositor-side.
   The blur is NOT part of the animation and never can be, so the
   surface is fully blurred from frame one and only its SHAPE and
   its RIM travel. That is the honest version of "the material
   forms": you are watching the piece of glass arrive, already
   being glass.

   The rim overshoots and settles, which is what does most of the
   work — a specular edge that flares and calms reads as something
   catching the light as it lands. */
/* Animates the INDEPENDENT scale/translate properties, never the
   transform shorthand. A surface that positions itself with a
   transform is completely normal — a centred floating capsule uses
   translateX(-50%) — and a keyframe touching `transform` replaces
   that outright, so the element slams sideways for the length of the
   entrance. scale: and translate: compose with an existing transform
   instead of overwriting it, which makes this safe to drop on any
   surface without first auditing how it is positioned.

   ONE CAVEAT, measured. The independent properties apply in the order
   translate, rotate, scale, THEN transform, so `scale` ends up outside
   the transform and multiplies it. On an element centred with
   `transform: translateX(-50%)` that means the -590px offset is scaled
   to -554px at scale .94, and the surface drifts 35px sideways for the
   length of the entrance before settling correct. Self-correcting, but
   visible.
   So: gel is for surfaces that APPEAR, positioned by layout. Do not put
   it on chrome that positions itself with a percentage transform — a
   centred floating bar, a translate-centred modal. Those want the rim
   flare alone, or nothing: a persistent element materialising on every
   load is noise, not craft. */
@keyframes dkGlassGelIn {
  from { scale: .94; translate: 0 6px; opacity: 0; }
  55%  { scale: 1.012; translate: 0 0;  opacity: 1; }
  to   { scale: 1;    translate: 0 0;  opacity: 1; }
}
@keyframes dkGlassRimFlare {
  from { --glass-rim-hi: .10; }
  45%  { --glass-rim-hi: 1;   }
  to   { --glass-rim-hi: .55; }
}
.dk-glass-gel {
  animation: dkGlassGelIn .46s cubic-bezier(.22,.9,.28,1) both;
}
.dk-glass-gel::before {
  animation: dkGlassRimFlare .62s cubic-bezier(.3,.7,.2,1) both;
}

/* ---- 14b · SPECULAR THAT FOLLOWS ------------------------------
   On device, Apple's rim highlight tracks tilt. On a pointer
   machine the equivalent is the cursor: the bright arc of the
   conic rim rotates so the surface always looks lit from wherever
   you are.

   --glass-rim-angle is @property-registered as an <angle> at the
   top of this section for one reason: an UNREGISTERED custom
   property is a string, and a string cannot interpolate, so the
   rim would snap between values instead of sweeping. Registering
   it is what makes the transition exist at all.

   COST: this repaints the ::before, which is a 1px ring — a paint,
   not a re-composite, and not the backdrop. Cheap, but it is still
   per-frame paint, so it is opt-in and belongs on ONE hero surface,
   never on a list. glass-motion.js rAF-coalesces the writes. */
@property --glass-rim-angle { syntax: "<angle>"; inherits: true; initial-value: 200deg; }
.dk-glass-track::before {
  background-image: conic-gradient(from var(--glass-rim-angle) at 50% 50%,
    rgb(var(--glass-hi) / var(--glass-rim-hi)),
    rgb(var(--glass-hi) / .06) 20%,
    rgb(var(--glass-hi) / 0)   40%,
    rgb(var(--glass-hi) / .28) 58%,
    rgb(var(--glass-hi) / var(--glass-rim-lo)) 76%,
    rgb(var(--glass-hi) / var(--glass-rim-hi)));
  transition: --glass-rim-angle .18s linear;
}

/* ---- 14c · THE MORPH, and why it is built inside out ----------
   Two glass surfaces flowing together is THE Liquid Glass
   behaviour — Apple does it with GlassEffectContainer and
   glassEffectID, and the whole reason that container exists is to
   let neighbouring shapes share a field and merge.

   THE OBVIOUS WEB VERSION DOES NOT WORK, and it fails invisibly.
   The standard metaball trick is feGaussianBlur + a feColorMatrix
   alpha contrast ramp applied to a CONTAINER. Measured here: a
   .dk-glass child inside `filter: url(#goo)` renders as a flat pale
   rectangle with no backdrop at all, while an identical sibling
   outside the container renders correctly. getComputedStyle
   reports the same backdrop-filter on both — section 10h's warning
   exactly. A container filter is a BACKDROP ROOT, so it truncates
   what every child can sample.

   SO THE MERGE MOVES INTO THE FIELD INSTEAD.
   glass-lens.js already builds its displacement from a signed
   distance function. A SMOOTH MINIMUM of two rounded boxes is a
   metaball union — it is how metaballs have always been done in
   SDF work — so one element can BE the merged shape:
     · its outline comes from a MASK painted from the same SDF
     · its refraction comes from a displacement map built from it
     · nothing filters an ancestor, so the glass lives
   A mask rather than a clip-path, and that choice is load-bearing:
   clip-path takes one polygon, so a SEPARATED pair cannot be
   expressed and an earlier version had to clamp the morph to the
   connected range to avoid closing the shape through the middle as
   a bowtie. A mask holds disjoint islands, so the morph now runs the
   full range. It also keeps the silhouette out of SVG filters
   entirely, so the SHAPE works on WebKit and Gecko even though the
   refraction gates itself off there.
   Verified: the lobes join through a real neck, separate cleanly,
   and the backdrop stays visible through all of it.

   BUDGET: the shape changes mean a new map per step, so this is
   for a DELIBERATE, short, user-triggered transition — a control
   opening into a menu — never an idle or scroll-driven loop. */
.dk-glass-morph {
  /* the element is the union of its parts; children position
     themselves inside it and must not draw their own surface */
  position: relative;
  isolation: isolate;
}
.dk-glass-morph > * { position: relative; z-index: 1; }

/* ---- 14d · guards --------------------------------------------
   Consistent with 10f: blur is static and stays, and everything
   that actually MOVES goes. The morph collapses to its end state
   rather than being removed, because the end state is the layout —
   removing it would leave the control in the wrong shape. */
@media (prefers-reduced-motion: reduce) {
  .dk-glass-gel,
  .dk-glass-gel::before { animation: none !important; }
  .dk-glass-track::before { transition: none !important; }
}
