A white page, and cards that still read on it

The canvas was #F1F0EC — a warm grey that made every screen look slightly
dull. It is white.

── You cannot have both ──

That canvas was not an accident: it was deepened from #F7F6F4 on 2026-09-23
precisely so a white card would sit *on* something and its 1px edge could go
quiet. A white card on a white page has nothing to sit on.

Measured rather than judged by eye: after the canvas change, a pixel scan
across a card boundary on the Orders golden read #FFFFFF from end to end. The
shadow contributed nothing findable. The cards had not become subtle, they had
disappeared.

So the edge comes back. `DmCard` drew its border only for tones that are not
white — the warm canvas was doing that job for the white ones — and now draws
it for every tone, with the lift still underneath. Border plus a soft shadow is
the iOS and Revolut shape rather than the borderless Airbnb one, and that is
the price of a white page. Paid deliberately.

── What moved with it ──

`surfaceAlt` lifts to #F4F2EF. With a white page it is the only token that
makes a surface read as recessed — filled inputs, tiles, skeletons, the sunken
card — so it can no longer sit within a point of the canvas.

`DmCardTone.sunken` filled itself with the canvas colour, which on a white page
is not sunken but invisible. It takes `surfaceAlt`.
This commit is contained in:
2026-09-30 11:35:30 +05:30
parent c7a74c57b8
commit e2fe1b1771
32 changed files with 47 additions and 19 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 252 KiB

View File

@@ -75,18 +75,34 @@ class DmColors {
static const ink4 = Color(0xFFB4AEAA);
// ── Surfaces ─────────────────────────────────────────────────────────────
/// The page canvas.
/// The page canvas. White.
///
/// Deepened on 2026-09-23. At `#F7F6F4` a white card on it was a 3% step,
/// so the 1px border was doing all the separating and every screen read as
/// a list of outlined boxes. A canvas the cards can sit *on* is what lets
/// the edge go quiet.
static const canvas = Color(0xFFF1F0EC);
/// ── This has been both, and the trade is real ──
///
/// It was `#F7F6F4`, then deepened to `#F1F0EC` on 2026-09-23 so a white
/// card would sit *on* something and the 1px edge could go quiet. That
/// worked, and it also made every screen read as slightly dull — which is
/// the note that brought it here.
///
/// You cannot have a white page and borderless cards: one of them has to do
/// the separating. On white, [DmCard] is held by its border and
/// [DmShadow.card] rather than by contrast, which is the iOS and Revolut
/// shape rather than the Airbnb one. That is the choice, made deliberately.
///
/// Everything that needs to read as *recessed* — filled inputs, the
/// segmented groove, a sunken card — moved to [surfaceAlt] and [groove],
/// which are the only tokens still darker than the page.
static const canvas = Color(0xFFFFFFFF);
static const surface = Color(0xFFFFFFFF);
/// Filled inputs, quiet chips, tiles, skeleton bases. Also the "sunken"
/// treatment for a disabled primary action.
static const surfaceAlt = Color(0xFFF1EFEC);
/// Filled inputs, quiet chips, tiles, skeleton bases, the "sunken" card and
/// the sunken treatment for a disabled primary action.
///
/// With a white canvas this is the *only* thing that makes a surface read as
/// recessed, so it carries more weight than it used to. It was within a
/// point of the old canvas, which is why the groove below had to exist at
/// all; on white it does its own job again.
static const surfaceAlt = Color(0xFFF4F2EF);
/// The bed a segmented control's chosen segment is raised out of.
///

View File

@@ -65,7 +65,9 @@ class DmCard extends StatelessWidget {
DmCardTone.plain => DmColors.surface,
DmCardTone.fill => DmColors.brand,
DmCardTone.wash => DmColors.brandSoft,
DmCardTone.sunken => DmColors.canvas,
// Not the canvas any more: the page is white, and a card filled with
// the page colour is not sunken, it is invisible.
DmCardTone.sunken => DmColors.surfaceAlt,
};
Color get _edge => switch (tone) {
@@ -106,15 +108,25 @@ class DmCard extends StatelessWidget {
boxShadow: _lifts ? (raised ? DmShadow.cardRaised : DmShadow.card) : null,
),
child: Container(
// A tone that is not white keeps its edge: a crimson or washed card is
// already separated from the canvas by its own colour, and a shadow
// under a saturated fill reads as grime rather than as lift.
foregroundDecoration: _lifts
? null
: BoxDecoration(
borderRadius: radius,
border: Border.all(color: _edge),
),
// ── Every card keeps its edge now ──
//
// A white card used to have none: the warm canvas underneath was doing
// the separating, and a border on top of that contrast read as an
// outlined box rather than a lifted surface.
//
// The page is white. A white card on it is invisible — measured, not
// guessed: a scan across a card boundary on the Orders golden was
// #FFFFFF from end to end, with the shadow contributing nothing the
// eye could find. One of the two has to do the separating, and with
// the page white it has to be the edge.
//
// The lift stays underneath it. Border plus a soft shadow is the iOS
// and Revolut shape; it is not the borderless Airbnb one, and that is
// the cost of a white page.
foregroundDecoration: BoxDecoration(
borderRadius: radius,
border: Border.all(color: _edge),
),
child: ClipRRect(
borderRadius: radius,
child: Column(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 KiB

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 252 KiB