/* * Liquid glass. * * Every surface is the same recipe: a thin wash of white, the ground behind it * blurred, a hairline that catches the light, and a highlight falling from the * top edge. It only reads as glass when there is something behind it to * refract, which is why this look brings its own ground — two soft drifting * blobs of the dashboard's own colours, or the image it was given. * * Dark is the natural direction for it. In light the same wash would be white * on white, so the mixture goes the other way: more opaque, brighter edges, * and a shadow soft enough not to read as a border. * * Paint only; the measurements are in `../core/core.css`. */ [data-look="glass"] { --gl-bg: rgb(255 255 255 / 0.1); --gl-bg-strong: rgb(255 255 255 / 0.18); --gl-border: rgb(255 255 255 / 0.2); --gl-highlight: rgb(255 255 255 / 0.22); --gl-inset: inset 0 1px 1px rgb(255 255 255 / 0.1); --gl-shadow: 0 8px 32px rgb(0 0 0 / 0.37); --gl-blur: 24px; --gl-glow: 0 0 22px color-mix(in srgb, var(--primary) 45%, transparent); --gl-radius: 1.25rem; --gl-track: rgb(255 255 255 / 0.16); --gl-blob-opacity: 0.5; } [data-look="glass"].light { --gl-bg: rgb(255 255 255 / 0.5); --gl-bg-strong: rgb(255 255 255 / 0.7); --gl-border: rgb(255 255 255 / 0.75); --gl-highlight: rgb(255 255 255 / 0.6); --gl-shadow: 0 8px 32px rgb(0 0 0 / 0.12); --gl-track: rgb(0 0 0 / 0.1); /* Diluted by the white it mixes into, so it takes more of it to show. */ --gl-blob-opacity: 0.6; } /* --- the ground ---------------------------------------------------------- */ /* * Glass has nothing to be glass against on a flat white page: a translucent * white tile over a white ground is a shadow and nothing else. So the look * brings a ground of its own — a wash of the dashboard's primary, deepest at * the top where a room's light would come from — and the tiles then sit * *brighter* than what is behind them, which is what reads as a pane. */ .gl-ground { position: absolute; inset: 0; background: radial-gradient( 120% 90% at 50% 0%, color-mix(in srgb, var(--primary) 14%, var(--background)), var(--background) 70% ); } .gl-blob { position: absolute; width: 55%; aspect-ratio: 1; border-radius: 50%; /* A gradient that falls off rather than a blurred circle: `filter: blur()` on something this size costs a full-screen pass every frame, and a panel on the wall is often the slowest machine in the house. */ background: radial-gradient( circle closest-side, var(--gl-blob, var(--primary)), transparent 100% ); opacity: var(--gl-blob-opacity, 0.55); } .gl-blob:nth-child(1) { left: -15%; top: -25%; --gl-blob: var(--primary); } .gl-blob:nth-child(2) { right: -20%; top: 5%; --gl-blob: var(--accent); } .gl-blob:nth-child(3) { left: 25%; bottom: -35%; opacity: calc(var(--gl-blob-opacity, 0.55) * 0.65); --gl-blob: var(--primary); } /* --- surfaces ------------------------------------------------------------ */ /* No `position` of its own: this is worn by a tile, a rail and a notice alike, and each of those is placed by whatever puts it there. Stating it here won over the utility that placed them — an unlayered rule beats a layered one — and collapsed the rail into the flow. */ .gl-surface { border: 1px solid var(--gl-border); border-radius: var(--gl-radius); background: var(--gl-bg); box-shadow: var(--gl-shadow), var(--gl-inset); backdrop-filter: blur(var(--gl-blur)); -webkit-backdrop-filter: blur(var(--gl-blur)); } /* * The light on the pane: a bright line along the top edge where it catches, * and a short wash falling from it. Carrying the wash the whole way down reads * as a gradient fill rather than as glass — the edge is what does the work. */ .gl-surface::before { content: ""; position: absolute; inset: 0; z-index: 0; border-radius: inherit; pointer-events: none; background: linear-gradient(to bottom, var(--gl-highlight), transparent 30%); opacity: 0.55; box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.28); } .gl-frame > * { position: relative; z-index: 1; } .gl-frame[data-selected] { outline: 2px solid var(--primary); outline-offset: -2px; } .gl-frame-title { color: var(--muted-foreground); } .gl-rail { border-radius: 9999px; } .gl-notice { border-radius: 9999px; color: var(--muted-foreground); } /* --- the glow a press leaves --------------------------------------------- */ .gl-pressable { position: relative; /* `clip` rather than `hidden`: the press ripple below is inset 0 and rounds with the surface regardless, but a segment's `--gl-glow` thumb sits inside this box too and needs room to fade rather than being cut off at it. */ overflow: clip; overflow-clip-margin: 1.5rem; isolation: isolate; } .gl-pressable > .gl-glow { position: absolute; inset: 0; z-index: 0; border-radius: inherit; pointer-events: none; opacity: 0; background: radial-gradient( circle at var(--press-x, 50%) var(--press-y, 50%), rgb(255 255 255 / 0.35), transparent 60% ); transition: opacity var(--duration-base) var(--ease-standard); } .gl-pressable:hover > .gl-glow, .gl-pressable:focus-visible > .gl-glow { opacity: 0.7; } .gl-pressable:active > .gl-glow { opacity: 1; } [data-touch] .gl-pressable:hover > .gl-glow { opacity: 0; } /* Lifted one at a time rather than by a blanket rule over every child: that one restated `position` on things already placed — a sliding pill, a switch's thumb — and dropped them back into the flow. */ .gl-switch-thumb, .gl-segment-label, .gl-lift { position: relative; z-index: 1; } /* --- controls ------------------------------------------------------------ */ .gl-button { display: inline-flex; min-width: 0; min-height: var(--dui-control); align-items: center; justify-content: center; gap: 0.5rem; border: 1px solid var(--gl-border); border-radius: 9999px; padding-inline: 1.25rem; font-size: var(--dui-text); font-weight: 500; background: var(--gl-bg); color: var(--foreground); box-shadow: var(--gl-inset); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } .gl-button[data-variant="filled"], .gl-button[aria-pressed="true"] { background: color-mix(in srgb, var(--primary) 78%, transparent); border-color: color-mix(in srgb, var(--primary) 55%, white); color: var(--primary-foreground); box-shadow: var(--gl-glow), var(--gl-inset); } .gl-button[data-variant="text"] { border-color: transparent; background: transparent; box-shadow: none; } .gl-button:disabled { opacity: 0.45; pointer-events: none; } .gl-switch { position: relative; display: inline-flex; width: calc(var(--dui-control) * 1.75); height: var(--dui-control); flex: none; align-items: center; justify-content: flex-start; border: 1px solid var(--gl-border); border-radius: 9999px; padding: 0.1875rem; background: var(--gl-track); box-shadow: var(--gl-inset); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } .gl-switch[data-state="on"] { justify-content: flex-end; background: color-mix(in srgb, var(--primary) 70%, transparent); box-shadow: var(--gl-glow), var(--gl-inset); } .gl-switch-thumb { display: block; height: calc(var(--dui-control) - 0.5rem); aspect-ratio: 1; border-radius: 50%; background: #fff; box-shadow: 0 2px 6px rgb(0 0 0 / 0.35); } .gl-switch:disabled { opacity: 0.45; pointer-events: none; } .gl-slider .dui-slider-rail { border-radius: 9999px; background: var(--gl-track); box-shadow: var(--gl-inset); } .gl-slider .dui-slider-fill { border-radius: 9999px; background: color-mix(in srgb, var(--primary) 85%, white); box-shadow: var(--gl-glow); } .gl-slider .dui-slider-thumb { background: #fff; border: 1px solid var(--gl-border); box-shadow: 0 2px 8px rgb(0 0 0 / 0.35); } .gl-segmented { position: relative; display: grid; min-width: 0; align-items: center; border: 1px solid var(--gl-border); border-radius: 9999px; padding: 0.25rem; background: var(--gl-bg); box-shadow: var(--gl-inset); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } .gl-segmented[data-orientation="vertical"] { height: 100%; grid-auto-flow: row; grid-auto-rows: minmax(0, 1fr); border-radius: 1.25rem; } .gl-segment { position: relative; display: flex; min-width: 0; min-height: calc(var(--dui-control) - 0.5rem); align-items: center; justify-content: center; border-radius: 9999px; padding-inline: 0.625rem; font-size: var(--dui-text); color: var(--muted-foreground); } .gl-segment[data-active] { color: var(--primary-foreground); } .gl-segment-thumb { position: absolute; inset: 0; z-index: 0; border-radius: 9999px; background: color-mix(in srgb, var(--primary) 85%, transparent); box-shadow: var(--gl-glow), var(--gl-inset); } .gl-segment-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .gl-field { display: flex; width: 100%; min-width: 0; min-height: var(--dui-control); align-items: center; gap: 0.5rem; border: 1px solid var(--gl-border); border-radius: 0.875rem; background: var(--gl-bg); padding-inline: 0.875rem; font-size: var(--dui-text); color: var(--foreground); text-align: left; box-shadow: var(--gl-inset); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); } .gl-field::placeholder { color: var(--muted-foreground); } .gl-field:disabled { opacity: 0.45; pointer-events: none; } .gl-menu { border: 1px solid var(--gl-border); border-radius: 0.875rem; background: color-mix(in srgb, var(--popover) 75%, transparent); color: var(--popover-foreground); box-shadow: var(--gl-shadow), var(--gl-inset); backdrop-filter: blur(var(--gl-blur)); -webkit-backdrop-filter: blur(var(--gl-blur)); } /* --- readings ------------------------------------------------------------ */ .gl-track { border-radius: 9999px; background: color-mix( in srgb, var(--dui-fill, var(--primary)) 22%, transparent ); box-shadow: var(--gl-inset); } .gl-fill { border-radius: 9999px; background: var(--dui-fill, var(--primary)); box-shadow: 0 0 14px color-mix(in srgb, var(--dui-fill, var(--primary)) 55%, transparent); } .gl-disc-thumb { border: 2px solid rgb(255 255 255 / 0.85); box-shadow: 0 2px 10px rgb(0 0 0 / 0.4); } /* --- focus --------------------------------------------------------------- */ [data-look="glass"] :is(.gl-button, .gl-switch, .gl-segment, .gl-field, .dui-disc):focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }