← ui

material

ui

what a control is made of, plain or glass, set once for a whole section

npx shadcn add @ag/material
Already installed? Preview an update
npx shadcn add @ag/material --dry-run

Your installed source stays unchanged until you update it. How updates work

Latest changes · 2026-10-15
  • A glass container's spill (GlassSpill, on every glass Panel) blurs only its band now: four strips along the edges instead of one layer the size of the container, which the browser blurred whole before the mask cut it to the band (a 632×475 panel: about 300,000 pixels a frame, now about 71,000). It draws the same: a pixel comparison of a profile panel shows no difference past the screen's dithering.
  • --glass-saturation, inherited: how much every layer of glass (a body, a front, a spill) deepens the colour behind it. 1, the default, is each layer's own deepening as before; 0 blurs the picture and leaves its colour as it is. Set it on any ancestor, the page's root included.
Updated 2026-10-15 · release notes

shelf glass & surfaces

used by editor, video, tooltip, dialog, command, slider, number, stepper, tree, menu, breadcrumb, components, account-menu, contacts-app, code, scroll-thumb, liquid-glass, glass-display, quote, ide, people-picker, input, backdrop, button, badge, label, separator, textarea, checkbox, radio-group, switch, select, toggle-group, rating, vote, thumbs, reactions, verdict, calendar, date-picker, resizable, tabs, spinner, skeleton, panel, group-box, readout, stat, country-picker, log, field, field-context, popover, hover-card, form, toast, alert, accordion, sheet, kbd, shortcut, thumb-strip, edge-arrows, progress, player, timeline, film-mosaic, deployment, trace, web-preview, email-address, browser, telegram-chat, social-profile

the demo above, verbatim

material.demo.tsx
"use client"

import * as React from "react"
import { Sample } from "@/app/samples"
import { Badge } from "@/registry/base-nova/ui/badge"
import { Button } from "@/registry/base-nova/ui/button"
import { Label } from "@/registry/base-nova/ui/label"
import {
  type Material,
  MaterialProvider,
} from "@/registry/base-nova/ui/material"
import { Glass } from "@/registry/base-nova/ui/material-glass"
import { Separator } from "@/registry/base-nova/ui/separator"
import { Textarea } from "@/registry/base-nova/ui/textarea"

// #region form
function Feedback() {
  return (
    <div className="flex w-72 flex-col gap-3">
      <div className="flex items-center justify-between">
        <Label htmlFor="feedback">feedback</Label>
        <Badge variant="secondary">beta</Badge>
      </div>
      <Textarea
        id="feedback"
        rows={2}
        placeholder="what should we fix first?"
      />
      <Separator />
      <div className="flex justify-end gap-2">
        <Button variant="ghost">cancel</Button>
        <Button>send</Button>
      </div>
    </div>
  )
}
// #endregion

// #region saturation
/** The same pane four times over one picture, `--glass-saturation` the only
 *  thing changing, in the light scheme and the dark. */
function Saturation() {
  const row = (scheme: "light" | "dark") => (
    <div className="grid grid-cols-4 gap-3">
      {[0, 1, 2, 3].map((s) => (
        <Glass
          key={s}
          scheme={scheme}
          className="h-28 p-3 font-mono text-xs"
          style={{ "--glass-saturation": s } as React.CSSProperties}
        >
          {scheme} {s}
        </Glass>
      ))}
    </div>
  )
  return (
    // The most colourful of the pictures (shore): over grey there is nothing to
    // deepen.
    <div className="grid gap-3 rounded-xl bg-[url(/lightbox/shore.jpg)] bg-cover bg-center p-6">
      {row("light")}
      {row("dark")}
    </div>
  )
}
// #endregion

export default function MaterialDemo() {
  const [material, setMaterial] = React.useState<Material>("glass")
  return (
    <div className="space-y-8">
      <Sample
        name="theme"
        title="one provider, every control"
        description="the same controls; the provider says what they are made of. Each can still say otherwise for itself (material on the control)."
        with="form"
      >
        <div className="flex flex-col gap-4">
          <div className="flex gap-2">
            <Button
              variant="outline"
              size="sm"
              pressed={material === "plain"}
              onClick={() => setMaterial("plain")}
            >
              plain
            </Button>
            <Button
              variant="outline"
              size="sm"
              pressed={material === "glass"}
              onClick={() => setMaterial("glass")}
            >
              glass
            </Button>
          </div>
          <MaterialProvider material={material}>
            <div className="rounded-xl bg-[url(/lightbox/peaks.jpg)] bg-cover bg-center p-6">
              <Feedback />
            </div>
          </MaterialProvider>
        </div>
      </Sample>
      <Sample
        name="glass"
        glance
        title="the glass itself"
        description="Glass is the material any element can wear (material-glass): the controls' glass faces are made of it."
      >
        <div className="rounded-xl bg-[url(/lightbox/shore.jpg)] bg-cover bg-center p-6">
          <Glass className="w-64 text-sm">a pane of glass</Glass>
        </div>
      </Sample>
      <Sample
        name="saturation"
        title="how much it deepens the colour"
        description="--glass-saturation, one thing changing across the row: 0 blurs the picture and keeps its colour, 1 is the default, past it deeper. Seen over colour; over a dark, grey page there is nothing to deepen."
        with="saturation"
      >
        <Saturation />
      </Sample>
    </div>
  )
}

the source, every file an install writes

material.tsx
"use client"

import * as React from "react"
import type { WornScheme } from "@/registry/base-nova/ui/material-glass"

// WHAT A CONTROL IS MADE OF: plain (the page's own surfaces, shadcn's look) or glass
// (liquid glass, the material in material-glass), and the colour SCHEME it stands in.
// Every base control takes `material` and `scheme` and draws the face they name; its
// behaviour is written once and is the same in both. A page, a panel or a player sets
// them for everything inside it with <MaterialProvider>, the theme; a prop on one
// control overrides it there. Nothing set, a control is plain, in the page's scheme.
//
//   <MaterialProvider material="glass" scheme="dark">   a player's chrome
//     <Button>save</Button>               glass, dark: from the provider
//     <Button material="plain">…</Button>  plain, its own choice, still dark
//   </MaterialProvider>
//
// A provider that names only one of the two keeps the other from the one around it.
//
// A SURFACE NEVER DECIDES ITS SCHEME; something decides for it:
//
//   auto          the page (its theme, the system's or a toggle's)
//   light, dark   the author, whatever the page is
//   backdrop      the pixels behind it: a picture under the surface, measured live
//
// Whoever decides, a surface has its two faces and wears one (`WornScheme`, through
// `schemeOf`). `backdrop` is answered by the nearest `Backdrop` source above it (a
// VideoPicture, a <Backdrop> picture: only what holds pixels can answer), per
// surface: a control that passes its element to `useScheme` is read by the part of
// the picture under IT, one that does not by the whole picture. The source also sets
// how thick its glass is (as thick as its words need over that picture, no thicker)
// and how strongly its rim reads.

export type Material = "plain" | "glass"
export type Scheme = WornScheme | "backdrop"
export type { WornScheme }

/** The tokens' tones, for what says how a thing stands. */
export type Tone = "ok" | "warn" | "bad"
/** A tone as WORDS on a surface (a readout's value, a group's name). The tones are
 *  drawn for a mark on any ground; as words on a light one they are deepened to a
 *  lightness of at most 0.55, where they read at about 4.5:1 on white (the warn tone
 *  as it is, 0.8, read at under 2:1). The dark half is the tone itself. */
export const TONE_WORDS: Record<Tone, string> = {
  ok: "text-[oklch(from_var(--tone-ok)_min(l,0.55)_c_h)] scheme-dark:text-tone-ok",
  warn: "text-[oklch(from_var(--tone-warn)_min(l,0.55)_c_h)] scheme-dark:text-tone-warn",
  bad: "text-[oklch(from_var(--tone-bad)_min(l,0.55)_c_h)] scheme-dark:text-tone-bad",
}

const MaterialContext = React.createContext<{
  material: Material
  scheme: Scheme
}>({ material: "plain", scheme: "auto" })

/** What a picture under the surfaces answers: the scheme the pixels under an element
 *  ask for (`null`: the whole picture), now and on every change. Made by whatever
 *  holds the pixels (ui/backdrop); `watch` returns the stop. */
export interface BackdropSource {
  watch(
    element: HTMLElement | null,
    onChange: (scheme: "light" | "dark") => void,
  ): () => void
}

const BackdropContext = React.createContext<BackdropSource | null>(null)

/** Makes `source` the answer to `scheme="backdrop"` for everything inside. */
export function BackdropScope({
  source,
  children,
}: {
  source: BackdropSource
  children: React.ReactNode
}) {
  return (
    <BackdropContext.Provider value={source}>
      {children}
    </BackdropContext.Provider>
  )
}

/** Sets the material and the scheme of every control inside it. Nested, the nearest
 *  one wins, for each of the two it names. */
export function MaterialProvider({
  material,
  scheme,
  children,
}: {
  material?: Material
  scheme?: Scheme
  children: React.ReactNode
}) {
  const around = React.useContext(MaterialContext)
  const value = React.useMemo(
    () => ({
      material: material ?? around.material,
      scheme: scheme ?? around.scheme,
    }),
    [material, scheme, around.material, around.scheme],
  )
  return (
    <MaterialContext.Provider value={value}>
      {children}
    </MaterialContext.Provider>
  )
}

/** The material a control draws: its own `material` prop when given, else the
 *  nearest provider's, else plain. */
export function useMaterial(own?: Material): Material {
  const around = React.useContext(MaterialContext)
  return own ?? around.material
}

/** The scheme a control wears: its own `scheme` prop when given, else the nearest
 *  provider's, else the page's; `backdrop` answered by the picture under `element`
 *  (the control's root; unset, the whole picture). Dark until the picture has been
 *  read, the scheme of a picture still loading. */
export function useScheme(
  own?: Scheme,
  element?: HTMLElement | null,
): WornScheme {
  const around = React.useContext(MaterialContext)
  const source = React.useContext(BackdropContext)
  const asked = own ?? around.scheme
  if (asked === "backdrop" && !source)
    throw new Error(
      'scheme="backdrop" with no picture above to read: place it inside a VideoPicture or a <Backdrop>, or name a scheme',
    )
  const [seen, setSeen] = React.useState<"light" | "dark">("dark")
  React.useEffect(() => {
    if (asked !== "backdrop" || !source) return
    return source.watch(element ?? null, setSeen)
  }, [asked, source, element])
  return asked === "backdrop" ? seen : asked
}

/** A SURFACE's scheme: `useScheme` read by the surface's own element, and the ref
 *  that element takes (merged with the consumer's `ref`). Every control that is a
 *  surface or stands alone wears it on its root:
 *
 *    const [s, here] = useSurfaceScheme(scheme, ref)
 *    return <div ref={here} className={cn(schemeOf(s), …)} />
 */
export function useSurfaceScheme<T extends HTMLElement>(
  own?: Scheme,
  ref?: React.Ref<T>,
): [WornScheme, (node: T | null) => void] {
  const [element, setElement] = React.useState<T | null>(null)
  const scheme = useScheme(own, element)
  const here = React.useCallback(
    (node: T | null) => {
      setElement(node)
      if (typeof ref === "function") ref(node)
      else if (ref) ref.current = node
    },
    [ref],
  )
  return [scheme, here]
}
material-glass.tsx
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"
import "./material-glass.css"

// THE GLASS MATERIAL, the one look every glass face is made of: the base controls'
// glass faces (button, badge, panel, textarea, …, `material="glass"`) and the whole
// liquid-glass collection read their glass from here, so glass is one material
// everywhere and a control that draws it pulls in this file, not the collection.
// Server-safe (no context, no state): `Glass` and `GlassEdge` render anywhere.

/** THE LIGHT ON A GLASS EDGE, defined once. Every edge of glass draws with these
 *  colours, whatever it is on: a control's glass (`glassVariants`), glass round media
 *  (`GlassEdge`), a lit pane inside a bar (`glassLift`), a pane's seam. All of it is
 *  derived; what is turned is the glass's state (`--glass-rim`), the light's colour
 *  (`--glass-rim-color`, `--glass-hue`) and the ground's (`--glass-ground-sheen`).
 *  Light and dark faces differ in their values here, never in how they are drawn. */
export const glassLight = [
  // THE STAIN, the one way glass takes a colour: `--glass-stain` (any CSS colour) at
  // `--glass-stain-amount` (20% unless set), whoever sets it: `tint` (glassStyle), a
  // parent staining every glass inside it, a control's state (on, invalid: a status
  // is stained as any colour is). From the stain alone, worked out here: its HUE,
  // the same colour made vivid and readable (lightness at least 0.78, chroma doubled
  // up to 0.2, a grey staying grey), which the light body and the rim wear, and its
  // DEEP hue, the same colour at depth (lightness at most 0.5) for the dark body,
  // where a light colour mixed in read as haze and the raw one as mud. Unstained,
  // both are invalid and every use falls back to the glass's own.
  "[--glass-hue:oklch(from_var(--glass-stain)_max(l,0.78)_min(calc(c*2),0.2)_h)] [--glass-hue-deep:oklch(from_var(--glass-stain)_min(l,0.5)_min(calc(c*2),0.2)_h)]",
  // The light around the glass: white, leaning toward the colour of what it stands
  // over (`--glass-ground`, the picture's colour under it, set by ui/backdrop), made
  // light and clear (lightness at least 0.85), by `--glass-ground-share` (35% unless
  // set). Light catching a curve takes the colour around it, a red rock's edge
  // warming, a sky's cooling (Apple's glass does; Adrian, 2026-10-11). No picture
  // read, white (the registered property's own, material-glass.css). Registered, so
  // a change of ground eases.
  "[--glass-ground-light:color-mix(in_oklab,oklch(from_var(--glass-ground)_max(l,0.85)_min(calc(c*1.5),0.2)_h)_var(--glass-ground-share,35%),white)]",
  // The light the rim catches: `--glass-rim-color` when set (an edge that says
  // something on its own), else the stain's hue, else the light around it. A colour
  // reads as itself on the edge, where the glass is brightest.
  "[--glass-rim-ink:var(--glass-rim-color,var(--glass-hue,var(--glass-ground-light)))]",
  // How strongly the rim reads against its ground, a multiple of all its light: the
  // same white catch reads several times stronger on dark glass ("overpowering" as a
  // full outline; as a corner catch alone, a touch less was right, Adrian,
  // 2026-10-11), so the dark face carries 0.8 of it. Over a picture the picture under
  // the glass decides (`--glass-ground-sheen`, set by ui/backdrop on the surface or on
  // a group round several, and inherited: the face's own number is only the fallback,
  // so a group's reading reaches every glass inside it).
  "[--glass-sheen:var(--glass-ground-sheen,1)] scheme-dark:[--glass-sheen:var(--glass-ground-sheen,0.8)]",
  // Glass ON glass (a field in a glass panel, a chosen segment in a glass bar):
  // what is under it is the outer glass, already frosted and tinted, never the
  // picture (the outer glass's blur makes it a backdrop root, so the inner one sees
  // only that). A full tint over it doubled the darkening into a flat, washed-out
  // slab ("too intense", Adrian, 2026-10-11), so inner glass carries
  // `--glass-on-glass` of its thickness (0.65), and `--glass-on-glass-rim` of its
  // edge's light (0.85): less than full, as the outer glass already tints and lights
  // what is under it, but at half of each an inner card read as faint, its body and
  // its soft edge (Adrian, 2026-10-11). Told by `--glass-body`, which every glass
  // BODY sets (`glassVariants`) and what is inside inherits; asked of the parent (a
  // container style query asks the nearest ancestor), so a body's own mark never
  // counts for itself. Not `data-material`: every control's root wears that for its
  // face, and a radio group's root, a slider's track or a tab list read as glass
  // stood on the picture at on-glass strength.
  "[--glass-stack:1] [--glass-stack-rim:1] [@container_style(--glass-body:1)]:[--glass-stack:var(--glass-on-glass,0.65)] [@container_style(--glass-body:1)]:[--glass-stack-rim:var(--glass-on-glass-rim,0.85)]",
  // How strong the edge's line is, the one multiple every part below reads: the
  // glass's state, its ground and what it stands on. At 1 the edge is the one set by
  // eye on frames over pictures, dark and light (Adrian, 2026-10-11), and every glass
  // wears it, a control's as a frame's. A look (`glassLooks`) carries a multiple of
  // its own, `--glass-look-rim`, inherited, so a page or a container can wear a look
  // while each glass in it keeps its own state.
  // A GHOST (`--glass-ghost`, 0 to 1: an inherited value, ui/setting) carries less
  // than half of every part of its light: what it shows applies, but is not set here.
  "[--glass-strength:calc(var(--glass-rim,1)*var(--glass-look-rim,1)*var(--glass-sheen,1)*var(--glass-stack-rim)*(1-0.55*var(--glass-ghost)))]",
  // The GLOW's strength, the soft light under the line: the same, without the
  // sheen. The sheen tunes the fine line to how strongly it reads against its
  // ground; the glow is the curve's body, the same at rest on either face.
  "[--glass-glow:calc(var(--glass-rim,1)*var(--glass-look-rim,1)*var(--glass-stack-rim)*(1-0.55*var(--glass-ghost)))]",
  // The catch at the corner facing the light (from the top left), at its brightest.
  "[--glass-catch:color-mix(in_oklab,var(--glass-rim-ink)_calc(25%*var(--glass-strength)),transparent)]",
  // The far corner, where the two faces differ as glass does. On the dark face a
  // fainter catch of light. On the light face, where a white catch has nothing to
  // stand against (light glass over a white sky showed no edge at all), the curve's
  // refraction: it bends darker light in, a faint dark band on the side away from
  // the light.
  "[--glass-far:rgb(24_32_44/calc(0.075*var(--glass-strength)))] scheme-dark:[--glass-far:color-mix(in_oklab,var(--glass-rim-ink)_calc(11%*var(--glass-strength)),transparent)]",
  // WHERE the light lies along the edge, from the glass's radius (`--glass-radius`, a
  // length every glass states once: it draws the corners too). A rounded corner's
  // edge begins (√2 − 1) × its radius along the light's 135° line from the box's
  // corner (`--glass-curve`), so the catch is brightest there (`--glass-corner`; the
  // far corner's `--glass-corner-far`, the same unless a shape squares one, a sheet
  // against the screen's edge, which sets it to 0). Taken as a share of the box's
  // diagonal it peaked at the shape only for a circle: a pill's catch began at 57%
  // and a wide field's at 81% of its light. From there each line falls to nothing
  // over 120 px (to 27.5% at 0.4375 of it), the fall the edge was set by eye with
  // on frames over pictures (a 372 × 209 frame, radius 20: within 4 px of it) and
  // about a 320 × 200 card's; never past the middle of the line, where the other
  // corner's begins (on a 40 px circle that is the fall it had, within 2 px).
  "[--glass-curve:calc(0.41421356*var(--glass-radius))] [--glass-corner:var(--glass-curve)] [--glass-corner-far:var(--glass-curve)] [--glass-fall:min(calc(var(--glass-corner)+120px),50%)] [--glass-fall-far:min(calc(var(--glass-corner-far)+120px),50%)]",
  // Each line's fall as the mask that draws it, from its own corner.
  "[--glass-catch-mask:linear-gradient(135deg,#000_var(--glass-corner),rgb(0_0_0/0.275)_calc(var(--glass-corner)+0.4375*(var(--glass-fall)-var(--glass-corner))),transparent_var(--glass-fall))] [--glass-far-mask:linear-gradient(315deg,#000_var(--glass-corner-far),rgb(0_0_0/0.275)_calc(var(--glass-corner-far)+0.4375*(var(--glass-fall-far)-var(--glass-corner-far))),transparent_var(--glass-fall-far))]",
  // The edge's knobs, each resolved here once, the only names read below: the
  // line's width and softness (1 px each unless set), the glow's depth (4 px), the
  // stain's amount (20%; a ghost's, a third of it).
  "[--glass-width:var(--glass-rim-width,1px)] [--glass-softness:var(--glass-rim-softness,1px)] [--glass-depth:var(--glass-rim-depth,4px)] [--glass-amount:calc(var(--glass-stain-amount,20%)*(1-0.65*var(--glass-ghost)))]",
  // The light inside the edge, the rim's soft body: a glow inside the lit corner
  // and a fainter one inside the far one, falling off over the depth (a hairline
  // beside a card, a thick band on a glass the size of an icon, which takes less),
  // at the glow's strength on either face.
  "[--glass-inner:inset_calc(var(--glass-depth)/4)_calc(var(--glass-depth)/2)_var(--glass-depth)_calc(var(--glass-depth)/-2)_color-mix(in_oklab,var(--glass-rim-ink)_calc(29%*var(--glass-glow)),transparent),inset_calc(var(--glass-depth)/-4)_calc(var(--glass-depth)/-2)_var(--glass-depth)_calc(var(--glass-depth)/-2)_color-mix(in_oklab,var(--glass-rim-ink)_calc(15%*var(--glass-glow)),transparent)]",
].join(" ")

/** The rim, drawn from that light: a SPECULAR edge, the light catching the curve where
 *  it faces the light, brightest at that corner, nothing along the sides, the far
 *  corner's own light (`--glass-far`) at the opposite one. Nothing on it is the same
 *  all the way round: a hairline round the whole shape and a dark one along the foot
 *  drew a grey outline, the opposite of a reflection ("a classic gray, a bit thicker
 *  line", Adrian, 2026-10-11). A whole pixel, softened by one, so it melts into the
 *  glow under it (`--glass-inner`) as one soft rim: a crisp half pixel read as a fine,
 *  harsh line, a soft one as a curve.
 *
 *  Two lines, one profile mirrored: the catch on `::before`, brightest where the
 *  SHAPE's edge begins along the light's line and falling over `--glass-fall`
 *  (`--glass-catch-mask`, worked out from the radius in `glassLight`); the far
 *  corner's on `::after`, the same from its own corner. Each is an inset shadow ring
 *  (it follows the radius and is anti-aliased like an SVG stroke in Chrome and
 *  WebKit) shaped by a gradient mask; a ring cut out by masks draws heavier round
 *  the curves than along the sides in Chrome at 2x (measured, 2026-10-11). Both
 *  pseudo-elements are the edge's: an element wearing it draws nothing of its own
 *  on them. */
const GLASS_RIM = [
  "before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:content-[''] before:shadow-[inset_0_0_var(--glass-softness)_var(--glass-width)_var(--glass-catch)] before:[mask-image:var(--glass-catch-mask)]",
  "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:content-[''] after:shadow-[inset_0_0_var(--glass-softness)_var(--glass-width)_var(--glass-far)] after:[mask-image:var(--glass-far-mask)]",
  "forced-colors:before:hidden forced-colors:after:hidden",
].join(" ")

/** The edge of glass alone, without its body: its light, its rim and the light inside
 *  it, on a POSITIONED element that is rounded as the glass is. For an edge with no
 *  glass body under it: glass round media (`GlassEdge`), a pane lit inside a bar
 *  (`glassLift`). A whole glass is `glassVariants`, which wears this same edge. */
export const glassEdge = `${glassLight} ${GLASS_RIM} rounded-(--glass-radius) shadow-(--glass-inner)`

/** Coloured words on glass (a field's error, a toned value or name): the rim's light
 *  along their foot, so they hold over the frost. */
export const glassWords =
  "[text-shadow:0_0.5px_0_color-mix(in_oklab,var(--glass-rim-ink,white)_35%,transparent)]"

/** A pane lit inside a glass (a bar's chosen segment): a faint fill of its words'
 *  colour, under glass's own edge. Absolute in the bar, rounded as its options are. */
export const glassLift = `bg-current/12 ${glassEdge}`

/** The shapes glass comes in, its box alone (the layout, the corners): what a piece
 *  shaped as glass and drawn by something else wears (a fluid glass, whose frost is
 *  a layer of its own). Each states its radius (`--glass-radius`); a round one
 *  (circle, pill, bar) is sized by `--glass-size` (its height; a circle's width
 *  too), its radius half of it, so a round glass made bigger stays round and its
 *  light stays at its curve: set `[--glass-size:3rem]`, never a size of its own.
 *  `surface` is the box alone: the control wearing it states its radius. */
export const glassShapes = {
  surface: "",
  circle:
    "inline-grid size-(--glass-size) shrink-0 place-items-center [--glass-size:2.5rem] [--glass-radius:calc(var(--glass-size)/2)]",
  pill: "inline-flex min-h-(--glass-size) items-center gap-2 px-4 py-2 [--glass-size:2.5rem] [--glass-radius:calc(var(--glass-size)/2)]",
  bar: "inline-flex h-(--glass-size) items-center gap-1 p-1.5 [--glass-size:3.25rem] [--glass-radius:calc(var(--glass-size)/2)]",
  card: "p-5 [--glass-radius:--theme(--radius-3xl)]",
} as const

/** LOOKS: a whole way of drawing the glass's light, worn by a glass or by anything
 *  round several (inherited, so a page can wear one), never a value tuned on one
 *  glass. Each glass keeps its own state (`--glass-rim`) inside it.
 *
 *  `apple`: the edge macOS renders on an app's icon, set by eye beside those icons
 *  (Adrian, 2026-10-11): a fine crisp line (half a pixel, no softness) a little brighter
 *  than the material's, a shallow light under it (2 px), and the body shaded across
 *  from the light's corner, a neutral grey lighter at the top left and darker at the
 *  bottom right. Glass among app icons wears it so it reads as one of them. */
export const glassLooks = {
  apple:
    "[--glass-look-rim:1.65] [--glass-rim-width:0.5px] [--glass-rim-softness:0px] [--glass-rim-depth:2px] [--glass-shade:1]",
} as const
export type GlassLook = keyof typeof glassLooks

/** The glass's BODY: the frosted, tinted face what it holds lies on. ONE glass,
 *  light or dark as the scheme it stands in says (`scheme-dark:`, from @ag/tokens).
 *  The light face frosts harder than the dark: its words are dark on a pale ground
 *  the picture's texture shows through, where the dark face's 60% black already
 *  hides it; at 5px a sky's clouds ran right under the words and the light glass read
 *  worse than the dark over the same picture (Adrian, 2026-10-11). The body is the
 *  face (`--glass-face`: `--glass-tint`, else the face's own at `--glass-thickness`)
 *  with the stain's hue mixed in; unstained, the face mixed with itself.
 *
 *  `--glass-body` marks a body, and only a body: what is inside stands on glass
 *  (`glassLight`'s stack). `data-material` is the face's, worn by every control
 *  whatever it stands on. */
/* HOW MUCH GLASS DEEPENS THE COLOUR behind it is one knob for every layer of it
 * (a body, a front, a spill): `--glass-saturation`, inherited, 1 by default (each
 * layer's own deepening: a body's a quarter more colour on dark), 0 none (the
 * picture's colour as it is, only blurred). Set on any ancestor, the page's root
 * included. Each layer writes it out as `saturate(calc(1+<its own>*var(--glass-
 * saturation,1)))`, in full: Tailwind finds a class only as written. */

const GLASS_BODY = [
  "[--glass-body:1]",
  // The face and the hue its stain takes on it (the light hue on the light face,
  // the deep one on the dark), mixed once; and what the glass does to the picture
  // behind it, `--glass-backdrop`, a whole filter in its order (a frost of its own,
  // a fluid glass's, replaces it there and nowhere else).
  // A ghost's body is a third as thick (`--glass-ghost`): the picture shows through
  // what is only inherited. How much it deepens the colour behind it is
  // `--glass-saturation` (above).
  "[--glass-face:var(--glass-tint,rgb(242_244_247/calc(var(--glass-thickness,0.7)*var(--glass-stack)*(1-0.65*var(--glass-ghost)))))] [--glass-body-hue:var(--glass-hue)] [--glass-backdrop:blur(var(--glass-blur,14px))_saturate(calc(1+0.4*var(--glass-saturation,1)))]",
  "scheme-dark:[--glass-face:var(--glass-tint,rgb(16_16_18/calc(var(--glass-thickness,0.6)*var(--glass-stack)*(1-0.65*var(--glass-ghost)))))] scheme-dark:[--glass-body-hue:var(--glass-hue-deep)] scheme-dark:[--glass-backdrop:blur(var(--glass-blur,6px))_saturate(calc(1+0.25*var(--glass-saturation,1)))]",
  "[-webkit-backdrop-filter:var(--glass-backdrop)] [backdrop-filter:var(--glass-backdrop)]",
  "bg-[color-mix(in_oklab,var(--glass-body-hue,var(--glass-face))_var(--glass-amount),var(--glass-face))] supports-[not(backdrop-filter:blur(1px))]:bg-background/95",
  // The body's SHADE, lit across from the light's corner as its edge is: lighter
  // where the light comes from (top left), darker at the far corner, neutral, over
  // the face. `--glass-shade` of it (0 unless a look sets it: the body is the face
  // alone). It is the glass's light, so it goes with its rim: a flat sheet with no
  // rim (a bar, a rail: `rim={0}`) has no shade, where a page-wide look laid a
  // light patch across the whole sheet (Adrian, 2026-10-11); a brighter state
  // keeps it as at rest.
  "[background-image:linear-gradient(135deg,rgb(255_255_255/calc(0.45*var(--glass-shade,0)*min(var(--glass-rim,1),1))),transparent_55%,rgb(24_32_44/calc(0.08*var(--glass-shade,0)*min(var(--glass-rim,1),1))))] scheme-dark:[background-image:linear-gradient(135deg,rgb(255_255_255/calc(0.12*var(--glass-shade,0)*min(var(--glass-rim,1),1))),transparent_55%,rgb(0_0_0/calc(0.3*var(--glass-shade,0)*min(var(--glass-rim,1),1))))]",
].join(" ")

/** CSS glass: a translucent body (`GLASS_BODY`) and the glass's edge (`glassLight`,
 *  `GLASS_RIM`), its drop shadow what parts it from the page. Keep the center clear
 *  enough for the backdrop to show through. The edge's pseudo-elements follow the
 *  radius without clipping content or focus rings. `edge: false` is the body alone,
 *  for glass whose edge is drawn above what it holds (a pane's); `edge: "only"` the
 *  edge and its shadow with no body at all, no fill and no backdrop filter (a glass
 *  `outline`: the rim alone, the picture untouched under it).
 * It sets `relative` (the rim hangs off it): in a `cn()` a placement of the surface's
 * own (`absolute`, `fixed`, `sticky`) comes AFTER it, or tailwind-merge keeps
 * `relative` and the surface falls into the flow.
 */
export const glassVariants = cva(
  [
    // Its corners are its radius, the one length its light is placed by too.
    "relative isolate bg-clip-padding rounded-(--glass-radius)",
    // Its rim's state is its own, 1 until it says otherwise: `--glass-rim` is
    // inherited (so a bare edge follows the glass it belongs to), and a glass inside
    // another took the outer one's state, a rail's flat sheet (`rim={0}`) putting out
    // the edge of every icon on it. A control's states set it after this.
    "[--glass-rim:1]",
    glassLight,
    // Focus is @ag/tokens' ring, the keyboard's only: a fine line just off the
    // glass's edge in the content's colour, following its radius.
    "disabled:pointer-events-none disabled:[&>svg]:opacity-40",
    // A change of scheme (the picture under it turned light or dark) eases: the body,
    // the words (what is inside inherits them as they move) and the light, never a
    // cut, over `--glass-change` (300 ms on the material's ease unless
    // `--glass-change-duration` or `--glass-change-ease` say otherwise); its rim's
    // state (`--glass-rim`, a hover, a focus) moves with it. The glass is the ONE
    // owner of its face's transition: a control's own motion goes in through
    // `--glass-moves`, whole items of `transition` (`opacity_150ms_ease-out,…`), never
    // a `transition-*`, `duration-*` or `ease-*` of its own, which tailwind-merge
    // weighed against these and dropped one or the other: a tooltip lost its fade,
    // an input its scheme ease (scripts/examples/glass-faces.tsx refuses one).
    // The shadow is never transitioned itself: the light in it eases through the
    // registered properties it is made of (`--glass-rim`, the sheen, the ground's
    // light), the shadow following them frame by frame. Transitioned as well, a layer
    // in the text's colour (an outline's ring, a pressed rim stained `currentColor`)
    // computed transparent for the whole transition, Chrome unable to blend a colour
    // still bound to the text: an outline lost its ring on every hover and every
    // leave (Adrian, 2026-10-11, measured: transparent from the hover to 300 ms).
    "[--glass-change:var(--glass-change-duration,300ms)_var(--glass-change-ease,var(--ease-out))] [transition:var(--glass-moves,background-color_0s),background-color_var(--glass-change),color_var(--glass-change),--glass-rim_var(--glass-change),--glass-ghost_var(--glass-change),--glass-sheen_var(--glass-change),--glass-ground-light_var(--glass-change)] motion-reduce:[transition:none]",
    "forced-colors:border",
    // Its words are the scheme's foreground, so everything inside reads on it with no
    // ink of its own; its drop shadow is the scheme's.
    "text-foreground scheme-light:text-zinc-950 [--glass-drop:0_2px_5px_-2px_rgb(24_32_44/0.24),0_8px_20px_-8px_rgb(24_32_44/0.22)] scheme-dark:[--glass-drop:0_4px_16px_-6px_rgb(0_0_0/0.28)]",
  ],
  {
    variants: {
      edge: {
        true: [
          GLASS_BODY,
          GLASS_RIM,
          "shadow-[var(--glass-drop),var(--glass-inner)]",
        ],
        false: [GLASS_BODY, "shadow-(--glass-drop)"],
        only: [GLASS_RIM, "shadow-[var(--glass-drop),var(--glass-inner)]"],
      },
      // The scheme the surface stands in: the page's (`auto`), or its own whatever
      // the page is. `dark` is the theme's own class on the surface, so the page's
      // colour variables flip there and in everything inside it: words, icons and
      // nested controls read on the glass by themselves. `light` (light glass over
      // a picture on a dark page) is the glass and its words only: the page's
      // variables have no light block to flip to, so what is inside reads in
      // `currentColor`, as every glass face's words do.
      scheme: {
        auto: "",
        light: "light",
        dark: "dark",
      },
      shape: glassShapes,
    },
    defaultVariants: { edge: true, scheme: "auto", shape: "card" },
  },
)

/** The colour scheme a surface WEARS: the page's, or light or dark whatever the page
 *  is. What a control is asked for may also be `backdrop` (material's `Scheme`),
 *  which is resolved to one of these before anything is drawn. */
export type WornScheme = NonNullable<
  VariantProps<typeof glassVariants>["scheme"]
>

/** The class a surface of either material wears for its scheme: the theme's `dark`
 *  (the page's colour variables flip on it and inside it), or `light` with light
 *  words (its glass and its `currentColor`; the page's variables stay). One helper,
 *  so no control spells it its own way. */
export const schemeOf = (scheme: WornScheme): string | undefined =>
  scheme === "auto" ? undefined : scheme
export type GlassShape = NonNullable<
  VariantProps<typeof glassVariants>["shape"]
>

/** Optional CSS controls; they can also be inherited from a parent or set in Tailwind. */
export type GlassStyle = React.CSSProperties & {
  "--glass-blur"?: string
  /** What the glass does to the picture behind it, a whole `backdrop-filter` in its
   *  order (unset, a blur of `--glass-blur` and the face's saturation). */
  "--glass-backdrop"?: string
  /** A control's own motion on its glass face, whole items of `transition`
   *  (`opacity 150ms ease-out, scale 150ms ease-out`). */
  "--glass-moves"?: string
  /** How long and on what curve the glass eases its own change (300 ms on the
   *  material's ease unless set). */
  "--glass-change-duration"?: string
  "--glass-change-ease"?: string
  /** A front surface's height (`GlassFront`), set by its owner, who pads what the
   *  glass holds by the same length. */
  "--glass-front-height"?: string
  /** How much a front surface blurs what passes under it (12px unless set). */
  "--glass-front-blur"?: string
  /** A front surface's own face, any CSS colour (unset, the page's background at
   *  55%, hued by the stain). */
  "--glass-front-color"?: string
  "--glass-tint"?: string
  /** The opacity of the face's own tint (0..1; 0.7 light, 0.6 dark unless set): a
   *  backdrop source sets it to what the words over its picture need. */
  "--glass-thickness"?: number | string
  /** How strongly the rim reads against its ground, a multiple of its light; unset,
   *  the face's own (1 light, 0.8 dark). A backdrop source sets it from the picture
   *  under the glass; inherited, so set on a group it reaches every glass inside. */
  "--glass-ground-sheen"?: number | string
  /** How bright the rim and its inner reflections are, a multiple of the default. */
  "--glass-rim"?: number | string
  /** A ghost, 0 to 1 (0 unless set): a value that applies but is not set here
   *  (ui/setting). At 1 the body is a third as thick, the rim and its glow under
   *  half as bright, the stain a third as strong. Not inherited; eased. */
  "--glass-ghost"?: number | string
  /** How thick the rim is. */
  "--glass-rim-width"?: string
  /** How soft the rim's line is, a blur (1px unless set). */
  "--glass-rim-softness"?: string
  /** How deep the soft light inside the rim reaches, a length (4px unless set). */
  "--glass-rim-depth"?: string
  /** A look's multiple of the edge's light, beside the glass's own state (1 unless
   *  a look sets it; inherited). */
  "--glass-look-rim"?: number | string
  /** How much the body is shaded across from the light's corner, 0 to 1 (0 unless a
   *  look sets it). */
  "--glass-shade"?: number | string
  /** The glass's corner radius, a LENGTH every glass states once (never a share:
   *  a percentage is not a radius). It draws the corners (`rounded-(--glass-radius)`)
   *  and places the edge's light at the curve. A glass rounded fully states half its
   *  height. Not inherited: a glass that states none draws square, its edge's light
   *  a whole ring. */
  "--glass-radius"?: string
  /** A round shape's height (a circle's width too), its radius half of it
   *  (`glassShapes`: 2.5rem unless set). */
  "--glass-size"?: string
  /** Where the far corner's light begins along the light's line, a length (unless
   *  set, the same as the lit corner's, from the radius): 0 for a shape whose far
   *  corner is square. */
  "--glass-corner-far"?: string
  /** Where the lit corner's light begins, likewise (0 for a square lit corner). */
  "--glass-corner"?: string
  /** The colour of the light the rim catches; unset, the stain's hue, else the light
   *  around the glass (white leaning toward its ground). */
  "--glass-rim-color"?: string
  /** A colour the glass is stained with, any CSS colour; set on a parent, every
   *  glass inside wears it. Its hue for the body and the rim is worked out from it
   *  (`glassLight`). */
  "--glass-stain"?: string
  /** How much of the stain is mixed in, a percentage (20% unless set). */
  "--glass-stain-amount"?: string
  /** How much of its tint a glass ON glass carries, a multiple (0.65 unless set):
   *  the outer glass already frosts and tints what is under it. */
  "--glass-on-glass"?: number | string
  /** How much of its edge's light a glass ON glass carries, a multiple (0.85
   *  unless set). */
  "--glass-on-glass-rim"?: number | string
  /** The colour of what the glass stands over; a backdrop source sets it from the
   *  picture under the glass. Its rim's light leans toward it. */
  "--glass-ground"?: string
  /** How far the rim's light leans toward `--glass-ground`, a percentage (35%
   *  unless set). */
  "--glass-ground-share"?: string
}

/** What a glass's light can be told, one by one: the props `Glass` takes, and
 *  what `glassStyle` turns into its custom properties for any element that wears
 *  `glassVariants` itself (a control's glass face). */
export interface GlassLight {
  /** The rim's brightness as a multiple of the default (1). A glass ring around
   *  an image that already carries its own edge wants more, so the two read as
   *  one family. Sets `--glass-rim`. */
  rim?: number
  /** The rim's thickness, any CSS length (default 1px). Sets `--glass-rim-width`. */
  rimWidth?: string
  /** The colour of the light on the rim, for an edge that says something on its
   *  own. Unset, the rim is the tint's hue, else the light around the glass (white
   *  leaning toward its ground). Sets `--glass-rim-color`. */
  rimColor?: string
  /** A colour to stain the glass with: a brand or a project's accent, so glass on
   *  its pages belongs to it, or a status (`var(--tone-ok)`: a success badge, an
   *  error toast, an invalid field), stained the same amount, the colour said again
   *  on its mark (an icon, a title, the rim) and never on all its words. Mixed into
   *  the face's own tint, never replacing it; unset, the glass is untouched. Sets
   *  `--glass-stain`, which a parent can set instead to stain every glass inside. */
  tint?: string
  /** How much of `tint` goes in, 0 to 1 (default 0.2, the material's, set by eye
   *  with the tint sample's knobs). Sets `--glass-stain-amount`. */
  tintAmount?: number
}

export type GlassProps<T extends React.ElementType = "div"> = {
  /** Render a native element or a component that forwards className and ref. */
  as?: T
  shape?: GlassShape
  scheme?: WornScheme
  className?: string
  style?: GlassStyle
} & GlassLight &
  Omit<
    React.ComponentPropsWithRef<T>,
    | "as"
    | "shape"
    | "scheme"
    | "rim"
    | "rimWidth"
    | "rimColor"
    | "tint"
    | "tintAmount"
    | "className"
    | "style"
  >

/** A glass's light as the custom properties `glassVariants` reads: nothing for what
 *  is unset, so a parent's (a tint set on a page) still reaches it, and no default of
 *  its own (the material's are in `glassLight` and the body). */
export function glassStyle({
  rim,
  rimWidth,
  rimColor,
  tint,
  tintAmount,
}: GlassLight): GlassStyle {
  return {
    ...(rim === undefined ? {} : { "--glass-rim": rim }),
    ...(rimWidth === undefined ? {} : { "--glass-rim-width": rimWidth }),
    ...(rimColor === undefined ? {} : { "--glass-rim-color": rimColor }),
    ...(tint === undefined ? {} : { "--glass-stain": tint }),
    ...(tintAmount === undefined
      ? {}
      : { "--glass-stain-amount": `${tintAmount * 100}%` }),
  }
}

/** Style with Tailwind, compose with `as`, and pass native props/ref directly. Its
 *  children are its content and nothing else: the glass draws on the element itself
 *  (`glassVariants`), its lines on its two pseudo-elements, which lie over content
 *  that is not positioned, its glow an inset shadow under the content. Content held
 *  to the edge (an icon in a ring of glass) covers the glow where it reaches; keep
 *  it a few pixels in (the glow lies within about 3 px of the edge: an image inset
 *  7% of a 40 px ring leaves nine tenths of it showing). */
export function Glass<T extends React.ElementType = "div">({
  as,
  shape = "card",
  scheme = "auto",
  rim,
  rimWidth,
  rimColor,
  tint,
  tintAmount,
  className,
  style,
  ...props
}: GlassProps<T>) {
  return React.createElement(as ?? "div", {
    "data-slot": "glass",
    "data-material": "glass",
    "data-shape": shape,
    "data-scheme": scheme,
    ...(as === "button" ? { type: "button" } : {}),
    ...props,
    style: {
      ...glassStyle({ rim, rimWidth, rimColor, tint, tintAmount }),
      ...style,
    },
    className: cn(glassVariants({ shape, scheme }), className),
  })
}

/** Glass as an edge only, over what runs to it: the material's own edge
 *  (`glassEdge`), nothing around it (no mat, no padding, no body). Over the box it is
 *  placed in (absolute, inset 0), at that box's `radius`. The one edge for glass round
 *  media, a display's frame (glass-display) and anything small that frames a picture
 *  in glass (the player's zoom finder), and for a pane whose edge lies over what it
 *  holds (a glass Panel). `scheme` is the face it shows, as a surface's: unset, the
 *  page's. Its brightness is `--glass-rim`, set on it or above it. */
export function GlassEdge({
  radius,
  scheme = "auto",
  className,
  style,
}: {
  /** The box's corner radius, a CSS length (never a percentage). */
  radius: string
  scheme?: WornScheme
  className?: string
  style?: GlassStyle
}) {
  return (
    <div
      aria-hidden
      data-slot="glass-edge"
      className={cn(
        glassEdge,
        schemeOf(scheme),
        "pointer-events-none absolute inset-0 z-30",
        className,
      )}
      style={{ "--glass-radius": radius, ...style } satisfies GlassStyle}
    />
  )
}

/** Which side of its glass a front surface lies along. */
export type GlassFrontAt = "top" | "bottom"

/** THE FRONT SURFACE: a strip of glass in FRONT of what a glass holds, across its top
 *  or along its foot, part of the glass and not a piece laid on it. Content passing
 *  under it (a board's columns under their names, a list scrolling under its tabs) is
 *  seen through it only as light: it blurs what is under it (`--glass-front-blur`,
 *  12px unless set) on a face of its own (`--glass-front-color`; unset, the page's
 *  background at 55%, hued by the glass's stain). Its corners are its glass's on its
 *  side (`rounded-*-[inherit]`, never cut by a clip: a blur cut at a rounded clip
 *  draws jagged corners). Its inner edge is one unbroken hairline (`::after`) lit by
 *  the rim's own catch from the side the light comes from, at 27.5% of the catch at
 *  the left (where the catch's first fall ends), dimming evenly to the right. It casts
 *  no shadow: its edge and its blur say where it begins, and a shadow with nothing
 *  under it is a smear. It works its light out itself (`glassLight`), from the inputs
 *  its glass sets (the stain, the rim, the scheme), so a stained glass stains it too.
 *
 *  These are the surface alone, placed by whoever wears them: `GlassFront` lays it
 *  over a glass (absolute, `--glass-front-height` tall); a list pinned over content
 *  that scrolls wears the classes on its own element (`sticky top-0`). Its glass must
 *  not blur on the element that holds it: a backdrop blur inside another sees only
 *  what is between the two, so a front inside a blurring body blurs nothing that
 *  passes under it (a glass Panel blurs on a back layer for that reason). */
const GLASS_FRONT = [
  "backdrop-blur-[var(--glass-front-blur,12px)] backdrop-saturate-[calc(1+0.25*var(--glass-saturation,1))]",
  glassLight,
  "bg-[color:var(--glass-front-color,color-mix(in_oklab,var(--glass-hue,color-mix(in_oklab,var(--background)_55%,transparent))_var(--glass-amount),color-mix(in_oklab,var(--background)_55%,transparent)))]",
  "after:pointer-events-none after:absolute after:inset-x-0 after:h-(--glass-width) after:content-[''] after:bg-[linear-gradient(90deg,color-mix(in_oklab,var(--glass-catch)_27.5%,transparent),color-mix(in_oklab,var(--glass-catch)_7.5%,transparent))]",
].join(" ")
export const glassFront: Record<GlassFrontAt, string> = {
  top: `${GLASS_FRONT} rounded-tl-[inherit] rounded-tr-[inherit] after:bottom-0`,
  bottom: `${GLASS_FRONT} rounded-bl-[inherit] rounded-br-[inherit] after:top-0`,
}

/** A front surface over its glass: absolute across the side it lies along (`at`),
 *  `--glass-front-height` tall (set by its owner, who pads what the glass holds by
 *  the same length), above what the glass holds (`z-10`). Place it inside the glass,
 *  a sibling of the content, with the glass's edge drawn over both (`GlassEdge`,
 *  above it): a face's own rim lies under anything positioned, the front included.
 *  Server-safe. */
export function GlassFront({
  at = "top",
  color,
  className,
  style,
  ...props
}: Omit<React.ComponentProps<"div">, "style" | "color"> & {
  at?: GlassFrontAt
  /** Its own face, any CSS colour, thin enough to stay glass; unset, the page's
   *  background at 55%, hued by the glass's stain. */
  color?: string
  style?: GlassStyle
}) {
  return (
    <div
      data-slot="glass-front"
      data-at={at}
      className={cn(
        "absolute inset-x-0 z-10 h-(--glass-front-height)",
        at === "top" ? "top-0" : "bottom-0",
        glassFront[at],
        className,
      )}
      style={
        color === undefined
          ? style
          : ({ "--glass-front-color": color, ...style } satisfies GlassStyle)
      }
      {...props}
    />
  )
}

/** THE BEND, where a curved edge bends the picture under it: a band just inside the
 *  edge where the picture is refracted, softened and a touch brighter (the curve
 *  gathers light from a wider patch, as a lens does), strongest at the edge and
 *  fading into the clear middle over about 14 px per unit of `bend` (`bendLayers`).
 *  What a frame over a picture has for depth, with no glass body to give it; a white
 *  glow there read as a wash, not as glass (Adrian, 2026-10-11).
 *
 *  Place it over the picture INSIDE the picture's rounded clip (absolute, inset 0,
 *  after the picture): a backdrop blur reads the square box behind it, and outside
 *  the clip that square's corners hold the page, which the blur dragged in as a dark
 *  smudge at every corner; inside, they hold picture, and the clip cuts the result
 *  smoothly.
 *
 *  Its shape is the edge's own: a rounded rectangle at the box's radius, its
 *  outline blurred, so the band follows the rim round every corner. Fades from the
 *  four straight sides stayed square at a rounded corner while the rim curved away,
 *  and the two read misaligned (Adrian, 2026-10-11).
 *
 *  ONE LIGHT with the edge's line: what the band lifts the picture by is the
 *  line's own strength (`--glass-strength`: the state, the look, the ground's sheen,
 *  glass on glass), worked out by `glassLight` on each layer that lifts (`lit`) as on
 *  the edge, from the same inputs, so line and bend rise and fall together on
 *  either face and the line stays the brightest, crispest thing. A fixed lift with a colour boost of its own
 *  burned a blue sky to cyan and swallowed the line (Adrian, 2026-10-11). No
 *  colour of its own: a bend softens and lifts, it does not saturate.
 *
 *  It STEEPENS into the edge, as a curved surface does: the surface tilts faster
 *  the nearer the edge, so the picture is barely touched until the last few pixels
 *  and then bent hard right at the rim. One blur faded in by one wide mask bent the
 *  edge no harder than 20 px in, an even haze ("the intensity right as we approach
 *  the end doesn't match real glass", Adrian, 2026-10-11). So it is LAYERS, each
 *  reaching less far and bending harder than the one under it (they compound,
 *  each blurring what the last left), the light's crest last: `bendLayers`. Each
 *  layer is `GLASS_BEND` with its filter (`--glass-bend-filter`) and its mask on
 *  `style`. */
export const GLASS_BEND =
  "pointer-events-none [backdrop-filter:var(--glass-bend-filter)] [-webkit-backdrop-filter:var(--glass-bend-filter)]"

/** The layers of the bend for a box of corner `radius` (px) and a `bend` (a
 *  multiple), outermost reach first: what each does to the picture under it (a
 *  backdrop filter) and where (a mask: the box's own rounded outline, stroked and
 *  blurred, so its fade follows the rim round every corner; an SVG with no size of
 *  its own, drawn at the size of the box it masks, its rectangle the box and only
 *  the radius given). From a faint softening reaching about 14 px per unit of bend,
 *  through a harder bend within about 6, to a hard one in the last 2 or 3, then
 *  the CREST, the light the curve gathers against the edge, which lifts the picture
 *  into the edge's fine line by a share of that line's own strength. A `lit` layer
 *  (the softening and the crest, which lift the picture by the light) wears
 *  `glassLight`; the others only bend. */
/** A mask of a box's own rounded outline (corner `radius`, a CSS length the SVG
 *  reads, px or rem, never a percentage, which an SVG reads against itself),
 *  stroked `stroke` px wide (half of it inside the box) and blurred by `blur` px: a
 *  band that follows the edge round every corner, fading inward. An SVG with no
 *  size of its own, drawn at the size of the box it masks. What the bend and the
 *  spill are shaped by. */
function edgeOutline(radius: string, stroke: number, blur: number): string {
  if (radius.includes("%"))
    throw new Error(
      `glass: an edge's radius is a length, never a percentage (got ${radius})`,
    )
  return `url("data:image/svg+xml,${encodeURIComponent(
    `<svg xmlns='http://www.w3.org/2000/svg'><filter id='b' x='-20%' y='-20%' width='140%' height='140%'><feGaussianBlur stdDeviation='${blur}'/></filter><rect width='100%' height='100%' rx='${radius}' fill='none' stroke='white' stroke-width='${stroke}' filter='url(#b)'/></svg>`,
  )}")`
}

export function bendLayers(
  radius: number,
  bend: number,
): { slot: string; filter: string; mask: string; lit: boolean }[] {
  const outline = (stroke: number, blur: number) =>
    edgeOutline(`${radius}px`, stroke, blur)
  return [
    {
      slot: "soften",
      filter: `blur(${1.2 * bend}px) brightness(calc(1 + 0.008 * var(--glass-strength)))`,
      mask: outline(10 * bend, 7 * bend),
      lit: true,
    },
    {
      slot: "bend",
      filter: `blur(${2.5 * bend}px)`,
      mask: outline(5 * bend, 2.5 * bend),
      lit: false,
    },
    {
      slot: "press",
      filter: `blur(${4 * bend}px)`,
      mask: outline(2.5 * bend, 1 * bend),
      lit: false,
    },
    {
      slot: "crest",
      filter: "blur(0.5px) brightness(calc(1 + 0.047 * var(--glass-strength)))",
      mask: outline(2.5 * bend, 0.8 * bend),
      lit: true,
    },
  ]
}

/** THE SPILL at a glass container's edge, over what it holds: a band of glass
 *  along the inside of its edge that blurs what lies under it, so what runs near
 *  the edge (rows scrolling, a picture) spills its light into the band and round
 *  the corners, as glass carries what is behind it; a box's padding between its
 *  content and its edge is then lit by that content, never a flat strip. No face,
 *  no line: the band is the blur alone, deepest at the edge and gone `width` in,
 *  following the rounded outline (`edgeOutline`). Subtle by default (4px, 10px
 *  in; 12px and 28 spread deep into a picture and ate it, Adrian 2026-10-14).
 *  Never a fade or a strip painted on.
 *
 *  A blur mixes both ways: at a corner where content meets the bare glass, the
 *  glass's own tone also reaches a little into the content. A one-way spill (the
 *  content's light out, never the glass's in) needs the content's light drawn
 *  under it, which no backdrop can do; until then it stays this subtle.
 *
 *  A glass CONTAINER wears it by default (`Panel material="glass"`), so nothing
 *  places it by hand. Placing one: inside the container, over
 *  what it holds and under its front surface and edge (absolute, inset 0, its
 *  corners the container's), with the same `radius` its `GlassEdge` takes. What it
 *  blurs must not be inside a backdrop root of its own (a masked scroller): it lies
 *  beside it, and nothing between it and the container may be one, or its blur
 *  reads nothing. Nor inside a box clipped by `overflow` with uneven or elliptical
 *  corners (`8px 8px 2px 2px`, `8px / 4px`): Chrome clips one through a mask of its
 *  own and drops the spill's, which then blurs everything it covers (measured
 *  2026-10-15, a laptop display's clip); such a box clips with `clip-path: inset(0
 *  round …)`. `--glass-spill-blur` sets the blur. Server-safe. */
export function GlassSpill({
  radius,
  width = 10,
  className,
}: {
  /** The container's corner, a CSS length (never a percentage): the outline the
   *  band follows, the one its `GlassEdge` takes. */
  radius: string
  /** How far in the band reaches, px: the last stretch of what nears the edge,
   *  never into what is being read. */
  width?: number
  className?: string
}) {
  if (!(width > 0))
    throw new Error(`GlassSpill: width is px, more than 0 (got ${width})`)
  // The outline stroked twice the band's width (half of it lies outside the box
  // and is cut there), its inner edge blurred so the blur eases in: it reaches
  // `reach` in, the stroke's inner half and the blur's tail (three of its
  // deviations).
  const mask = edgeOutline(radius, width * 2, width / 3)
  const reach = width * 2
  // A backdrop is blurred over its element's whole box before any mask cuts it,
  // so one layer the size of the container blurred all of it every frame (a
  // 632×475 panel: 300,000 pixels for a band of 10). The band is four strips
  // instead, each blurring only the edge it lies along: the top and the foot as
  // deep as the corner and the band (`side`), the sides between them. Each wears
  // the one mask, sized to the whole container (its `cq` units: the wrapper is
  // the container) and placed where the strip lies in it, so the four draw the
  // band the single layer drew.
  // In the container's own units, never a percentage: a mask's position reads a
  // percentage against the strip, not the container.
  const side = `min(calc(${radius} + ${reach}px), 50cqh)`
  const strip = (
    at: React.CSSProperties,
    position: string,
  ): React.CSSProperties => ({
    ...at,
    maskImage: mask,
    WebkitMaskImage: mask,
    maskSize: "100cqw 100cqh",
    WebkitMaskSize: "100cqw 100cqh",
    maskPosition: position,
    WebkitMaskPosition: position,
    maskRepeat: "no-repeat",
    WebkitMaskRepeat: "no-repeat",
  })
  const blur =
    "absolute [backdrop-filter:blur(var(--glass-spill-blur,4px))_saturate(calc(1+0.2*var(--glass-saturation,1)))] [-webkit-backdrop-filter:blur(var(--glass-spill-blur,4px))_saturate(calc(1+0.2*var(--glass-saturation,1)))]"
  return (
    <div
      aria-hidden
      data-slot="glass-spill"
      className={cn(
        "pointer-events-none absolute inset-0 rounded-[inherit] [container-type:size]",
        className,
      )}
    >
      {/* Rounded as the container is, so no blur shows past its corners: a
          backdrop is clipped by its own element's radius. */}
      <div
        className={blur}
        style={strip(
          {
            top: 0,
            left: 0,
            right: 0,
            height: side,
            borderTopLeftRadius: radius,
            borderTopRightRadius: radius,
          },
          "0 0",
        )}
      />
      <div
        className={blur}
        style={strip(
          {
            bottom: 0,
            left: 0,
            right: 0,
            height: side,
            borderBottomLeftRadius: radius,
            borderBottomRightRadius: radius,
          },
          "0 100%",
        )}
      />
      <div
        className={blur}
        style={strip(
          { top: side, bottom: side, left: 0, width: reach },
          `0 calc(-1 * ${side})`,
        )}
      />
      <div
        className={blur}
        style={strip(
          { top: side, bottom: side, right: 0, width: reach },
          `100% calc(-1 * ${side})`,
        )}
      />
    </div>
  )
}
material-glass.css
/* The rim's brightness as a registered number, so it can ease: an unregistered
   custom property swaps at once, which made a glass's light jump while the
   glass itself (a lifting card, a pressed button) was still easing. Inherited,
   so a bare edge (a pane's, a frame's) follows the glass it belongs to; a glass
   inside starts again at its own (`glassVariants`). It eases on the element that
   draws the light, never on a container: an inherited number moving every frame
   restyles everything under it. */
@property --glass-rim {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}

/* A ghost, 0 to 1: a value that applies but is not set here (ui/setting). A glass
   rim is light, not a line, so it cannot be dashed the way a plain face's edge is;
   a ghost is the glass itself thinner, its light softer, its stain fainter
   (`glassLight`, the body). Registered so it eases with the glass when a value is
   set or cleared, and so its absence is 0. Not inherited: a ghost's thumb or a
   field inside it is not a ghost of its own. */
@property --glass-ghost {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* The glass's corner radius, the one length its corners and its edge's light are
   placed by (`glassLight`). Registered only to stop it inheriting: a glass inside
   another that states no radius of its own must not wear the outer one's, it draws
   square with its light a whole ring, which shows at once. Any value, no initial. */
@property --glass-radius {
  syntax: "*";
  inherits: false;
}

/* How strongly the rim reads against what it stands on, a multiple of its light
   (`--glass-rim`, the glass's own state, is the other). Worked out on each element
   that wears the light (`glassLight`): the ground's (`--glass-ground-sheen`, a
   picture's, from ui/backdrop) when there is one, else the face's own. Registered
   so it eases with the glass when the ground or the face changes; not inherited,
   since every element that reads it declares it, so its easing restyles only the
   elements that draw with it. */
@property --glass-sheen {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

/* The light around a glass, the colour its rim catches (`glassLight`): white
   leaning toward the picture under it. Registered for two reasons: so it eases
   when what is under the glass changes colour, and so a glass over no picture
   (its formula invalid without `--glass-ground`) falls back to white, this
   property's own value. Not inherited, for the same reason as the sheen. */
@property --glass-ground-light {
  syntax: "<color>";
  inherits: false;
  initial-value: white;
}