Signed off 2026-08-02. The dashboard moves to a frosted surface over a brand field, and the rest of the estate follows page by page.
Mock of the whole dashboard in it:
docs/styling-review-2026-07/glass-dashboard.html, and the candidates it was
chosen from: docs/styling-review-2026-07/card-idiom.html.
Why this and not the alternatives: a rounded box with a hairline and a shadow is what every tool ships, which is why our cards read as generic. A coloured rail on a rounded card is worse, because it is the single most recognisable machine-made pattern going. Glass is a real decision about the product’s surface rather than a decoration applied to a box.
What makes it ours rather than everyone’s is the squared corner: 14px radius on three corners, 3px on the bottom-left, carried down to sidebar items, thumbnails and speech bubbles. Glass on its own is a trend. Glass with a silhouette is an identity.
Decided 2026-08-04. It was raised for the browser extension Panel, which
turned out not to be a flush surface: it sits right: 16px, so it floats
clear and takes the normal 14/14/14/3. The rule below stands for anything
genuinely flush, and is recorded because it will be right the first time
somebody builds one.
A related trap the same investigation turned up: that Panel is top: 0 with
height: 100%, so it is flush vertically even though it floats
horizontally. Every one of its corners sits on the viewport’s top or bottom
edge. Check every edge before deciding a surface floats, not just the one
that prompted the question. The fix there is to inset it vertically as well and
let the normal corner apply, rather than to invent a variant.
The signature survives, and reads more clearly. The visible corners on a right-flush panel are top-left and bottom-left, and the 3px is the bottom-left. The one corner carrying the signature is the one still on screen. On a normal card it is one squared corner competing with three rounded ones; on a flush panel it is one squared against one rounded, a straight 1:1 contrast.
The rule for a flush surface is 0 14 0 3. Square off the corners that meet
the viewport edge rather than rounding them. A radius flush against the edge
leaves a sliver of the page showing through the curve, which reads as a
rendering fault rather than as a radius.
This is the same signature seen from the side, not a different one. Do not “fix” it back to four corners.
1. It is a change to the page, not to the card. backdrop-filter blurs
what is behind it, and a flat page tint blurs to itself. Without the gradient
field there is no glass, only a pale rectangle. Any page adopting this adopts
the field first.
2. Large panels only. Every glass surface is a live blur of what is under it, recomputed on scroll. It belongs on panels, not on 46px rows: thirty blurring shortcut cards cost real frames and the effect is invisible at that size anyway.
3. Text has to be checked against the field, not against white. Measured
against the darkest point the field puts under a card, which composites to
rgb(236,228,238) at 62% white, a heading holds 10.59 and body text 5.33, but
the eyebrow lavender lands at 4.25 and fails. Opaque enough to rescue it is
80% white, which is not glass. So on glass the eyebrow darkens to body ink.
This turned out not to be a one-off. The house muted ink fails on glass too.
--ts-color-muted (#6b6880) is calibrated at 5.36:1 on white and 4.69:1 on
the page tint, and measures 4.31:1 on glass. That is not a fault in the
token: glass is simply darker than either surface it was calibrated against.
So converted pages step it one darker, to #5b5872 (5.48:1), and
unconverted pages keep the calibrated value because it is right for the surface
they are on. It is set in one place, glass.css, keyed on the body class, so
each new page inherits it rather than needing its own contrast patch. The audit
page needed one before this rule existed; the SEO page was about to need a
second, which is the point at which a patch should become a rule.
The grey reaches the page three ways and all three need covering: the theme
variable --paragraph, the design token --ts-color-muted, and Bootstrap’s
.text-muted, which needs !important because the utilities are !important.
This has now caught us twice, in two codebases, from opposite directions:
Two independent instances of one rule. Text contrast is not the whole check. Anything placed on a coloured surface needs measuring against that surface: icons, fills, borders, dividers, and any state indicator.
The thresholds differ, which is part of why this gets missed: 4.5:1 for text, 3:1 for non-text such as a fill, an icon or a boundary. A state indicator can pass its text check and still fail as an indicator.
There is a console sweep for the interaction-state half of this on Checking contrast. Run it on any page you convert: hover and focus states are the ones that cannot be caught by looking.
4. It is all or nothing per page. Glass on the dashboard beside flat white on the audit page looks broken, not transitional. Convert a whole page or none of it.
Measured 2026-08-04 for the extension, whose dark panel colour is #3e3e3e.
A dark panel over a dark field has almost no range to work with. Compositing
rgba(62,62,62,alpha) over a brand field on #1d1d1d, the difference between
the panel over the field’s lightest and darkest points is:
| panel alpha | field showing through |
|---|---|
| .62 | 1.05 |
| .70 | 1.03 |
| .76 | 1.03 |
| .88 | 1.01 |
1.05 is nothing, and lowering the alpha does not rescue it: .62 is already as good as it gets. Getting real range would need washes bright enough to look garish on dark.
So on dark: flat panel, hairline border, squared corner. Keep the field and the blur for light. Spending a gradient layer, a live blur and a calibration pass on an effect that measures 1.05 buys nothing, and the signature does not depend on the frost. It lives in the corner, the border and the ink discipline, all of which survive on a flat surface.
The active state inverts on dark. On light only a solid brand fill clears the 3:1 state threshold, at 7.09. On dark the same brand fails:
| fill on dark | vs panel |
|---|---|
brand #712F79 |
1.28 fails |
lightened #8f4a97 |
1.93 fails |
lightest primary #b07fb8 |
3.53 passes |
And on that fill, white text fails at 3.19 while dark #1d1d1d text passes at
5.28. So the dark active band is a light fill with dark text: the inverse of
light mode, not a recolour of it. Assuming symmetry here ships an invisible
state.
Blur is free to drop. Contrast is a function of the composite colour, not
the blur radius, so a blur token can go to 0 under load without any of these
numbers moving and without re-measuring.
This was left open, with a note that it might want to stay solid as the one opaque element saying “this is the frame, the rest is content”. Built on a real page, it does not. The sidebar is a floating frosted panel, and it is the largest piece of glass on the page.
It keeps its 250px footprint and only the visible panel is inset, by 12px, so the content beside it does not move. Same 14/3 corner as every other panel, so it reads as part of the same family rather than as chrome that happens to be translucent.
The field has to move to body for this. A frosted panel needs something
behind it, and if the field only covers the content area then the sidebar is
frosting a flat white strip, which looks like a rendering fault. This is cost
one again: it is a change to the page, not to the element.
Inset it with margins, never with top. The header is fixed and its offset
varies with the viewer: 0 normally, 40px with the admin toolbar, 80px with its
tray open. A hardcoded top clipped the first nav item under the header for
admins only, which is a fault you will not see unless you happen to be logged
in as one.
Re-ink the text. Cost three bites here harder than anywhere else. Against
the darkest point the field puts under the panel, compositing to
rgb(236,228,238) at 62% white:
| on white | on glass | verdict | |
|---|---|---|---|
nav link #6b6880 16px |
5.9 | 4.31 | fails |
section label #858b98 14px |
3.42 | 2.75 | already failed, worse |
replacement #5b5872 |
7.4 | 5.48 | passes |
16px is not large text, so 4.5 is the bar for all of them. The labels take the same ink as the links rather than a lighter one, because a lighter ink is what failed; they still read as secondary because they are smaller, uppercase and letterspaced, so the hierarchy was never carried by colour alone.
The active item is a band welded to the frame. Updated 2026-08-03 after the first version was tried and rejected.
style.css gives every sidebar link border-left: 3px solid transparent and
turns it purple when active. That is the left accent rail this whole direction
rejected, and it survived only because it predates the decision. Floating the
sidebar is what made it obvious: an edge marker on a panel that no longer has
an edge to sit against.
The first replacement was a brighter well. It looked like a white rectangle, and there is a number behind that: it separated from the frosted panel by 1.17:1, against the 3:1 a non-text state indicator needs. It was not merely plain, it was barely perceivable as a state.
A tint does not fix it. Any wash over frosted glass barely shifts the surface: brand at 10% measured 1.17, at 16% measured 1.29. This is worth remembering for any future state on this surface. Only a solid fill clears the threshold, at 7.09, with white text at 8.81.
Solid brand is not a new idea here: the next-steps panel already uses a filled brand block with white text for its dominant state.
The band bleeds to the left edge rather than floating inside the panel. A detached rectangle in the middle of a list says “this one is different”; a band welded to the frame says “this is where you are”. Square on the left, rounded on the right: a relative of the house 14/3 corner rather than the same shape, because the signature belongs to cards and panels and this is a band welded to an edge.
Three things that only showed up on a real page, not in the mock:
margin-right pushed it
past the edge instead of insetting it. It needs width: auto.<img> files, not inline SVG. Each item ships deselect, sm
and active variants, and the active one is brand purple because it was drawn
for a pale well. No stroke or fill rule can reach inside an <img>. Use
filter: brightness(0) invert(1), which flattens any artwork to white and so
cannot be broken by the artwork changing later.A rounded panel wants clipping, not scrolling. Found 2026-08-05, and it was self-inflicted by the change above.
Floating the sidebar gave it a radius, and a long menu spilled past the
corner. The obvious fix was overflow-y: auto on the panel. But
.sidebar-list-item inside it has carried overflow: auto since
style.css:334, so the panel became a scroller wrapped around a scroller.
On pages whose sidebar carries extra blocks below the list, the help-support
menu or the editor thumbnail panel, the shell overflowed too and drew a second
bar beside the inner one. That is why it appeared intermittently rather than
everywhere, and why it read as a glitch rather than as a layout bug.
overflow: hidden gives the radius the clipping it actually needed and adds
no scroller. The companion is min-height: 0 on the scrolling child, so it
absorbs the shrink instead of forcing the shell to overflow: without it the
footer below the list gets clipped away rather than staying reachable.
The general rule: before adding overflow to a container, check whether a
descendant already scrolls. Reaching for overflow to fix a visual spill
is how you get two scrollbars, because clipping and scrolling are different
jobs and auto does both.
Worth knowing how this was found, because it is not visible at rest: walk the
sidebar’s descendants for computed overflowY of auto or scroll and count
how many have scrollHeight > clientHeight. More than one is the bug. A
screenshot at a comfortable window size shows nothing at all.
Still open: the header strip is solid and now sits above a floating sidebar. It reads fine, but it is the one piece of chrome not decided either way.
The glass panel rules name .card, .middle-content-block and .ts-card.
A page built from a different vocabulary gets the field and the sidebar but
keeps its own panels, which reads as a page the conversion forgot rather than
as a page that was never listed.
That is exactly what happened to connected accounts. It uses .ts-card, from
connected-accounts.css and social-composer.css, and has no .card
anywhere on it, so it sat on the glass field with solid white panels and a
uniform 12px radius. Adding .ts-card to the panel selector fixed it: the
scoped rule outranks the component’s own, so the glass background, radius and
shadow replace its white fill and inset hairline while its padding survives.
When a converted page still looks wrong, check what its panels are actually called before assuming the route list is at fault. See Checking buttons for the same trap in the button sweep, where it is worse: there it returns a clean result rather than an obviously wrong page.
Add its route to _tubespanner_purple_glass_pages() in
tubespanner_purple.theme. That adds a ts-glass-page body class and the
whole surface follows. That list is the only record of what is converted.
It is a body class rather than a class in the markup because there are 75 page templates, each carrying its own copy of the dashboard wrappers. Converting by editing markup would mean touching 75 files and would guarantee a half-converted page somewhere. The sidebar also sits outside the content wrapper, so a class on the content could never reach it.
Converted so far: the dashboard, the channel audit, and the Discovery and SEO Optimiser page.
Each conversion so far has needed nothing page-specific beyond the route. The two things that did come up, nested panels drawing a second surface and muted text failing contrast, were both fixed as general rules, so the next page should inherit them.
Until a page moves, cards stay white and the state colour is an accent, not a surface and not an edge. See Outcome states for where the colour is spent.
This is not a lesser design waiting to be replaced. It is what a card looks like when the state is carried by content rather than by decoration, and the glass version keeps every one of these rules on top of a different surface.
.ts-card is taken. It is defined in
css/components/connected-accounts.css and .ts-card-title is used by the
social post composer and calendar. A global .ts-card silently restyles them.
The house class is .ts-stack-card.
A filled animation outranks a normal declaration. The dashboard tool row
animates its cards in with animation-fill-mode: both, ending on
transform: none. That value keeps applying for good and beats any transform
declared in CSS, so a card idiom using one renders as a plain rectangle and
looks like the stylesheet never landed. Use backwards unless the end state is
genuinely meant to be permanent.
The signature is 14px on three corners, 3px bottom-left. Small surfaces take 12px on three corners, 3px bottom-left.
| surface | radius |
|---|---|
| panel: a section of the page | 14px 14px 14px 3px |
| small surface: a shortcut row, a tile | 12px 12px 12px 3px |
The squared corner is constant. Only the rounded ones step down, because a 46px row with a 14px radius reads as rounder than a 600px panel with the same value: the same number is a larger proportion of a smaller box.
Worth knowing because it looks like drift. A sweep of the dashboard finds 34 surfaces at 12px and 4 at 14px and reports an inconsistency, and QA raised exactly that. It is a two-step scale, not two decisions. The bottom-left is 3px in both, which is the part that actually carries the identity.
The same rule decides the blur: small surfaces stay opaque. A live
backdrop-filter on dozens of 46px rows costs a compositing pass each and
buys nothing at that size.