`overflow: hidden` on the frame and its body cut a control's 22px `--gl-glow` dead at the clip edge, and the gauge's own `<svg>` cut its drop-shadow at the viewBox — the arc's top sits ~3.5 units from it. `overflow: clip` with a margin wide enough for the blur (1.5rem on the body, which already sits 1rem inside the frame; 0.5rem is all that leaves the tile, which is the gutter). That means the body no longer scrolls by default. `.dui-frame-scroll` was already wired through from `scrolls()` into every look's Surfaces with no CSS rule behind it, so the class is what opts markdown, agenda and notification back into a real scroller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KYM38KSb4V4v2T71eifnZv
418 lines
10 KiB
CSS
418 lines
10 KiB
CSS
/*
|
|
* 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;
|
|
}
|