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`.
594 lines
24 KiB
Dart
594 lines
24 KiB
Dart
import 'package:flutter/material.dart';
|
||
|
||
/// Design tokens. Every colour, radius, shadow and text style in the app comes
|
||
/// from here — screens never hardcode a value.
|
||
///
|
||
/// Direction (2026-09-15 rebuild, from the 14-screen design):
|
||
///
|
||
/// **Edges, not elevation.** Surfaces are separated by a 1px warm hairline and
|
||
/// a 10pt radius, not by a shadow. The only shadows left in the system are the
|
||
/// crimson glow under the one committing action on a screen, the float under a
|
||
/// control that genuinely hovers over a map, and the sheet lift. A screen that
|
||
/// drops shadows everywhere has no way left to say "this one matters".
|
||
///
|
||
/// **One accent, three intensities.** Crimson fills the header and the primary
|
||
/// action, washes (`brandSoft` over `brandLine`) carry supporting information,
|
||
/// and everything else is warm neutral. There is no second hue competing for
|
||
/// meaning — `ok` survives only for the delivered state, `danger` only for
|
||
/// destructive text.
|
||
///
|
||
/// **Poppins for voice, Geist Mono for data.** Headings run at w800 with tight
|
||
/// negative tracking; anything a customer might read back aloud or check
|
||
/// digit-by-digit — a booking reference, a phone number, a fare, a plate, a
|
||
/// countdown — is set in mono so the glyphs line up and a transposed character
|
||
/// is visible.
|
||
class DmColors {
|
||
DmColors._();
|
||
|
||
// ── Brand ────────────────────────────────────────────────────────────────
|
||
/// The mark's single colour.
|
||
///
|
||
/// Briefly brightened to a vermilion on 2026-09-23 to match a reference
|
||
/// design and put straight back. The palette is not up for redesign — the
|
||
/// reference's *layout* is what the app is being drawn against, and the
|
||
/// oxblood carries it perfectly well.
|
||
static const brand = Color(0xFF8F0F06);
|
||
|
||
/// Pressed state, and brand text sitting on a [brandSoft] wash where the
|
||
/// full brand would vibrate against the tint.
|
||
static const brandPress = Color(0xFF730C05);
|
||
static const brandDeep = brandPress;
|
||
|
||
/// The lit edge of the sphere and of the primary button's gradient. Named
|
||
/// rather than repeated as a literal in three files.
|
||
static const brandLit = Color(0xFFA6180B);
|
||
|
||
/// What the launch fills the screen with, before anything else exists.
|
||
///
|
||
/// ── Why this is not [brand] ──
|
||
///
|
||
/// It is the launcher icon's own crimson, and it matches on purpose: the
|
||
/// customer taps a red tile and the screen fills with that same red, which
|
||
/// reads as one continuous gesture rather than as an app starting up. [brand]
|
||
/// is the UI's red and is four shades deeper; using it here would make the
|
||
/// tap and the screen after it two different colours for no reason a customer
|
||
/// could name.
|
||
///
|
||
/// The two never appear together — the splash has inverted to white before a
|
||
/// single piece of UI is drawn.
|
||
///
|
||
/// Mirrored in `android/app/src/main/res/values/colors.xml` as `dm_splash_bg`
|
||
/// and in `ios/Runner/Base.lproj/LaunchScreen.storyboard`, because the window
|
||
/// is painted this colour from process start — long before Flutter can.
|
||
static const splash = Color(0xFFBD0921);
|
||
|
||
/// Tint behind selected options, wash cards and brand chips.
|
||
static const brandSoft = Color(0xFFFBF3F2);
|
||
|
||
/// The wash's own edge, and the outline on quiet brand buttons.
|
||
static const brandLine = Color(0xFFF0DEDC);
|
||
|
||
// ── Ink — warm neutrals, never pure black ────────────────────────────────
|
||
static const ink = Color(0xFF191716);
|
||
static const ink2 = Color(0xFF5C5654);
|
||
static const ink3 = Color(0xFF6E6864);
|
||
static const ink4 = Color(0xFFB4AEAA);
|
||
|
||
// ── Surfaces ─────────────────────────────────────────────────────────────
|
||
/// The page canvas. White.
|
||
///
|
||
/// ── 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, 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.
|
||
///
|
||
/// A step below [surfaceAlt], which is within a point of the canvas and so
|
||
/// drew no groove at all on a sheet. A segment only reads as *raised* if
|
||
/// there is something visible for it to be raised out of.
|
||
static const groove = Color(0xFFE7E4E0);
|
||
|
||
/// Barely-there divider between rows inside one card.
|
||
static const hairline = Color(0xFFF1EFEC);
|
||
|
||
/// A card's own edge — the workhorse of this design.
|
||
static const border = Color(0xFFE8E5E2);
|
||
|
||
// ── On-brand text ────────────────────────────────────────────────────────
|
||
/// Secondary text on a crimson field. Not white-at-opacity: a tinted rose
|
||
/// keeps its warmth where translucent white goes grey and dirty.
|
||
static const onBrand2 = Color(0xFFFFD9D4);
|
||
|
||
/// Tertiary text on a crimson field.
|
||
static const onBrand3 = Color(0xFFFFD1CB);
|
||
|
||
/// Recessed track inside the header (the segmented control's bed).
|
||
static const onBrandSunken = Color(0x24000000);
|
||
|
||
/// A control resting on the crimson field.
|
||
static const onBrandRaise = Color(0x2EFFFFFF);
|
||
|
||
// ── Semantic ─────────────────────────────────────────────────────────────
|
||
static const ok = Color(0xFF0A6B56);
|
||
static const okSoft = Color(0xFFE2F4EF);
|
||
static const danger = Color(0xFFB3231F);
|
||
|
||
/// "You are here" on a map. Deliberately not the brand crimson — on a map
|
||
/// crimson means the pickup pin, and blue is the convention every customer
|
||
/// already reads as their own position.
|
||
static const locationDot = Color(0xFF1B6EF3);
|
||
static const locationHalo = Color(0x241B6EF3);
|
||
|
||
static const scrim = Color(0x80191716);
|
||
|
||
// Backwards-compatible aliases used across the widget layer.
|
||
static const bg = canvas;
|
||
static const surface2 = surfaceAlt;
|
||
static const line = hairline;
|
||
static const lineStrong = border;
|
||
}
|
||
|
||
class DmRadius {
|
||
DmRadius._();
|
||
|
||
// Every corner was opened up on 2026-09-22. At 8–12 the app measured
|
||
// correct and looked like a form: a screen of tight rectangles, which is
|
||
// exactly the "boxy" the redesign was called in to fix. Four points across
|
||
// the scale is the difference between a panel and a card, and it costs
|
||
// nothing but the token.
|
||
|
||
/// Tiles, chips, the small square that holds a glyph.
|
||
static const xs = 10.0;
|
||
|
||
/// Buttons, fields, icon buttons. The design's most-used corner.
|
||
static const sm = 12.0;
|
||
|
||
/// Cards and every panel that holds rows.
|
||
static const md = 18.0;
|
||
|
||
/// A card that stands alone on the canvas rather than in a stack of rows.
|
||
/// Wider than [md] because a shadowed card with no edge needs the corner to
|
||
/// carry the shape the border used to.
|
||
static const card = 20.0;
|
||
|
||
/// A chip: a status pill, a tier badge, a stat tile's glyph well.
|
||
static const chip = 8.0;
|
||
|
||
/// Sheets' grabber area, the segmented control, the header's bottom sweep.
|
||
static const lg = 16.0;
|
||
|
||
/// Bottom sheets and the auth sheet that overlaps the hero. Moved with the
|
||
/// scale — at 16 it had collided with [lg], and a sheet that shares a corner
|
||
/// with the cards inside it has stopped being a separate surface.
|
||
static const xl = 20.0;
|
||
|
||
/// The brand header's bottom corners.
|
||
static const header = 26.0;
|
||
|
||
static const pill = 999.0;
|
||
|
||
/// A primary bar's corner. Softer than a field's, because the committing
|
||
/// action is the one element on a screen allowed to look like an object
|
||
/// rather than part of the form.
|
||
static const book = 16.0;
|
||
|
||
static BorderRadius all(double r) => BorderRadius.circular(r);
|
||
}
|
||
|
||
class DmSpace {
|
||
DmSpace._();
|
||
|
||
/// Horizontal page padding.
|
||
///
|
||
/// 24 once, when the header was a crimson field and the canvas held loose
|
||
/// rows. On a page of white cards it left a broad margin down both sides and
|
||
/// the cards read as narrower than the screen — the reference designs this
|
||
/// was drawn against run their cards to within about a finger of the edge.
|
||
static const pad = 16.0;
|
||
static const headerPad = 16.0;
|
||
|
||
/// Minimum touch target.
|
||
static const tap = 48.0;
|
||
|
||
/// Primary button height.
|
||
static const button = 54.0;
|
||
|
||
/// Field height — matched to [button] so a form and its action share a rhythm.
|
||
static const field = 54.0;
|
||
}
|
||
|
||
class DmShadow {
|
||
DmShadow._();
|
||
|
||
/// Under the one committing action on a screen, and under a selected slot.
|
||
static const lift = [
|
||
BoxShadow(
|
||
color: Color(0x808F0F06),
|
||
blurRadius: 24,
|
||
spreadRadius: -12,
|
||
offset: Offset(0, 10),
|
||
),
|
||
];
|
||
|
||
/// A smaller version of [lift] for chips and half-width controls.
|
||
static const liftSm = [
|
||
BoxShadow(
|
||
color: Color(0xA68F0F06),
|
||
blurRadius: 16,
|
||
spreadRadius: -8,
|
||
offset: Offset(0, 7),
|
||
),
|
||
];
|
||
|
||
/// A card resting on the canvas.
|
||
///
|
||
/// ── Why the cards stopped having edges ──
|
||
///
|
||
/// Every surface in this app was separated by a 1px hairline, which is
|
||
/// honest and cheap and, repeated eight times down a screen, reads as a
|
||
/// wireframe: the outline is the loudest thing about an object whose whole
|
||
/// job is to be quiet and hold something else.
|
||
///
|
||
/// Two very soft layers do it instead — a wide, faint ambient wash and a
|
||
/// short contact shadow just under the shape. Neither is visible as a
|
||
/// shadow; together they lift the white off the warm canvas so the card is
|
||
/// read as *nearer* rather than as *outlined*. That is the difference
|
||
/// between a premium consumer surface and a bordered panel, and it costs
|
||
/// nothing but two `BoxShadow`s.
|
||
static const card = [
|
||
BoxShadow(
|
||
color: Color(0x0D1F1A17),
|
||
blurRadius: 22,
|
||
spreadRadius: -10,
|
||
offset: Offset(0, 8),
|
||
),
|
||
BoxShadow(
|
||
color: Color(0x081F1A17),
|
||
blurRadius: 3,
|
||
spreadRadius: -1,
|
||
offset: Offset(0, 1),
|
||
),
|
||
];
|
||
|
||
/// A card the customer has to act on, or the one live thing in a list.
|
||
/// The same two layers, a third again as deep.
|
||
static const cardRaised = [
|
||
BoxShadow(
|
||
color: Color(0x141F1A17),
|
||
blurRadius: 30,
|
||
spreadRadius: -12,
|
||
offset: Offset(0, 10),
|
||
),
|
||
BoxShadow(
|
||
color: Color(0x0B1F1A17),
|
||
blurRadius: 4,
|
||
spreadRadius: -1,
|
||
offset: Offset(0, 2),
|
||
),
|
||
];
|
||
|
||
/// Controls that genuinely float over content — map buttons, map chips.
|
||
static const float = [
|
||
BoxShadow(
|
||
color: Color(0x0D191716),
|
||
blurRadius: 2,
|
||
offset: Offset(0, 1),
|
||
),
|
||
];
|
||
|
||
static const sheet = [
|
||
BoxShadow(
|
||
color: Color(0x2E191716),
|
||
blurRadius: 40,
|
||
spreadRadius: -20,
|
||
offset: Offset(0, -8),
|
||
),
|
||
];
|
||
|
||
// Aliases kept for existing call sites.
|
||
static const two = float;
|
||
static const three = sheet;
|
||
}
|
||
|
||
/// Clearance for the floating tab bar.
|
||
///
|
||
/// ── It was being counted twice ──
|
||
///
|
||
/// Every root padded its foot by `MediaQuery.paddingOf(context).bottom + 74`,
|
||
/// on the reasoning that the inset is the device's home indicator and 74 is
|
||
/// the bar floating over it. That is not what the body is handed. `Scaffold`
|
||
/// with `extendBody: true` **replaces** the body's bottom padding with the
|
||
/// laid-out height of the `bottomNavigationBar` — the bar's own SafeArea, its
|
||
/// 10 of padding and its 62 of pill, all of it. Measured: a 34pt device inset
|
||
/// and this bar gives the body `padding.bottom = 106`, which is the bar
|
||
/// exactly.
|
||
///
|
||
/// So the roots were clearing the bar, and then clearing it again. It cost 74
|
||
/// points of empty canvas under the last card on Home, Orders and Account —
|
||
/// enough that Home's caption had nowhere to go while a hole sat under the
|
||
/// form.
|
||
///
|
||
/// The inset is the clearance. [gap] is the air between the last card and the
|
||
/// top of the pill, which is a design decision rather than a measurement.
|
||
double dmTabBarClear(BuildContext context, {double gap = 12}) =>
|
||
MediaQuery.paddingOf(context).bottom + gap;
|
||
|
||
class DmMotion {
|
||
DmMotion._();
|
||
static const fast = Duration(milliseconds: 140);
|
||
static const base = Duration(milliseconds: 250);
|
||
static const slow = Duration(milliseconds: 460);
|
||
static const ease = Curves.easeOutCubic;
|
||
|
||
/// The bottom nav's travelling pill — a touch of overshoot, which reads as
|
||
/// the indicator catching up rather than teleporting.
|
||
static const glide = Cubic(0.34, 1.06, 0.36, 1);
|
||
|
||
/// Screen transitions. An emphasised decelerate: it leaves quickly and
|
||
/// settles slowly, so a push feels driven at the start and arrived-at by the
|
||
/// end. `easeOutCubic` is too even across a distance this short — it reads
|
||
/// as a constant-speed slide.
|
||
static const enter = Cubic(0.05, 0.7, 0.1, 1);
|
||
}
|
||
|
||
/// Poppins for voice, Geist Mono for data.
|
||
///
|
||
/// ── Why the sans styles look different from the mono ones ──
|
||
///
|
||
/// Poppins has no variable release. Google Fonts ships it as static cuts only,
|
||
/// so the sans styles below set `fontWeight` and nothing else: there is no
|
||
/// `wght` axis to move, and `fontVariations` on a static font is ignored in
|
||
/// silence, which is worse than useless — it reads as if it were doing the
|
||
/// work. `pubspec.yaml` declares one file per weight and Flutter matches on the
|
||
/// enum. Geist Mono is still variable and its three styles still carry the axis.
|
||
///
|
||
/// Only the four weights the design uses are bundled — 500, 600, 700, 800 —
|
||
/// because each one is a separate ~155 KB file. Asking for w900 anywhere would
|
||
/// not fail; Flutter would quietly synthesise it from Bold, and it would look
|
||
/// like it.
|
||
///
|
||
/// ── Every weight is one step lighter than it was ──
|
||
///
|
||
/// Poppins is a geometric face built on near-circles, and it carries far more
|
||
/// ink at the same nominal weight than the humanist faces before it. Styles
|
||
/// that were w900 are w800, w800 are w700, w700 are w600. Set at the old
|
||
/// numbers the headings looked inflated rather than strong.
|
||
///
|
||
/// ── And the tracking is deeper again ──
|
||
///
|
||
/// The opposite correction from the last face. Poppins is wide, round and has
|
||
/// generous default sidebearings, so display sizes need pulling in harder than
|
||
/// anything this app has used — the header runs at −1.2. Body sizes stay near
|
||
/// zero, because the same roundness is what makes it legible small and tracking
|
||
/// it in takes that back. Tracking belongs to a typeface, not to a design.
|
||
class DmText {
|
||
DmText._();
|
||
|
||
/// ── Switzer → Plus Jakarta Sans (2026-09-24) ──
|
||
///
|
||
/// Both are variable neo-grotesques and the swap is a one-line change, but
|
||
/// two things come with it. Plus Jakarta's humanist curves and taller
|
||
/// x-height give the place names the editorial weight the reference design
|
||
/// has, and — the practical part — **it carries the rupee glyph**, which
|
||
/// Switzer does not. Every amount in this app was set in Geist Mono for that
|
||
/// reason alone; the ones that are prices rather than data can now be type.
|
||
///
|
||
/// Its axis is wght 200–800, so every weight the design uses is real rather
|
||
/// than synthesised.
|
||
static const _f = 'PlusJakartaSans';
|
||
static const _mono = 'GeistMono';
|
||
|
||
// The weight axis. Switzer and Geist Mono both ship as one variable file, so
|
||
// a style has to move `wght` to change weight — `fontWeight` alone picks the
|
||
// nearest named instance and, on a single-file VF, that is always the
|
||
// default. Both are set on every style: the axis does the work, the enum
|
||
// keeps the semantics (and the system-font fallback) correct.
|
||
//
|
||
// This came back when Poppins left. Poppins has no variable release, so for
|
||
// one release the sans styles carried `fontWeight` alone and this block was
|
||
// the mono's private business.
|
||
static const _w500 = [FontVariation('wght', 500)];
|
||
static const _w600 = [FontVariation('wght', 600)];
|
||
static const _w700 = [FontVariation('wght', 700)];
|
||
static const _w800 = [FontVariation('wght', 800)];
|
||
|
||
// Letter spacing is quoted in em by the design and converted here at each
|
||
// size, because -0.035em is a different number of pixels on a 31pt display
|
||
// head than on a 15pt row title, and splitting the difference would soften
|
||
// exactly the heads that are supposed to feel tight.
|
||
|
||
// ── Display ──────────────────────────────────────────────────────────────
|
||
|
||
/// Screen-dominating statement — "Pickup booked", "Arun is 12 minutes away".
|
||
static const display = TextStyle(
|
||
fontFamily: _f, fontSize: 31, height: 1.1,
|
||
fontWeight: FontWeight.w800, fontVariations: _w800,
|
||
letterSpacing: -1.1, color: DmColors.ink,
|
||
);
|
||
|
||
/// The largest line on a screen: the name on Home, the title of a tab root.
|
||
///
|
||
/// It was white, because it was written for a crimson header. That header is
|
||
/// a canvas now and every caller was overriding the colour back to ink, so
|
||
/// the white was a trap waiting for the first caller who did not.
|
||
static const headerTitle = TextStyle(
|
||
fontFamily: _f, fontSize: 34, height: 1.0,
|
||
fontWeight: FontWeight.w800, fontVariations: _w800,
|
||
letterSpacing: -1.4, color: DmColors.ink,
|
||
);
|
||
|
||
/// Screen headings on a canvas.
|
||
static const title = TextStyle(
|
||
fontFamily: _f, fontSize: 25, height: 1.16,
|
||
fontWeight: FontWeight.w800, fontVariations: _w800,
|
||
letterSpacing: -0.8, color: DmColors.ink,
|
||
);
|
||
|
||
/// Tab-root page titles where no brand header is used.
|
||
static const pageTitle = title;
|
||
|
||
/// The heading above a group of cards, and a sheet's own title.
|
||
static const heading = TextStyle(
|
||
fontFamily: _f, fontSize: 18, height: 1.25,
|
||
fontWeight: FontWeight.w700, fontVariations: _w700,
|
||
letterSpacing: -0.45, color: DmColors.ink,
|
||
);
|
||
|
||
static const sheetTitle = TextStyle(
|
||
fontFamily: _f, fontSize: 21, height: 1.2,
|
||
fontWeight: FontWeight.w800, fontVariations: _w800,
|
||
letterSpacing: -0.65, color: DmColors.ink,
|
||
);
|
||
|
||
// ── Rows ─────────────────────────────────────────────────────────────────
|
||
|
||
/// A row's own line — `t1` in the design, and the workhorse of the app.
|
||
static const cardTitle = TextStyle(
|
||
fontFamily: _f, fontSize: 15, height: 1.4,
|
||
fontWeight: FontWeight.w600, fontVariations: _w600,
|
||
letterSpacing: -0.2, color: DmColors.ink,
|
||
);
|
||
|
||
/// Alias: the same weight of statement, used mid-form.
|
||
static const bodyStrong = cardTitle;
|
||
|
||
static const body = TextStyle(
|
||
fontFamily: _f, fontSize: 15, height: 1.5,
|
||
fontWeight: FontWeight.w500, fontVariations: _w500,
|
||
letterSpacing: -0.12, color: DmColors.ink,
|
||
);
|
||
|
||
/// Supporting line under a row title — `t2`.
|
||
static const small = TextStyle(
|
||
fontFamily: _f, fontSize: 13.5, height: 1.45,
|
||
fontWeight: FontWeight.w500, fontVariations: _w500,
|
||
color: DmColors.ink2,
|
||
);
|
||
|
||
/// The quietest line — `t3`. Timestamps, counts, "collected on".
|
||
static const tiny = TextStyle(
|
||
fontFamily: _f, fontSize: 13, height: 1.4,
|
||
fontWeight: FontWeight.w500, fontVariations: _w500,
|
||
color: DmColors.ink3,
|
||
);
|
||
|
||
/// Paragraph under a screen heading.
|
||
static const lede = TextStyle(
|
||
fontFamily: _f, fontSize: 14.5, height: 1.55,
|
||
fontWeight: FontWeight.w500, fontVariations: _w500,
|
||
color: DmColors.ink2,
|
||
);
|
||
|
||
/// The right-hand value in a settings row.
|
||
static const value = TextStyle(
|
||
fontFamily: _f, fontSize: 13.5, height: 1.4,
|
||
fontWeight: FontWeight.w600, fontVariations: _w600,
|
||
color: DmColors.ink2,
|
||
);
|
||
|
||
/// Field labels, tags, chips, inline actions.
|
||
static const label = TextStyle(
|
||
fontFamily: _f, fontSize: 12.5, height: 1.3,
|
||
fontWeight: FontWeight.w600, fontVariations: _w600,
|
||
color: DmColors.ink2,
|
||
);
|
||
|
||
static const button = TextStyle(
|
||
fontFamily: _f, fontSize: 16, height: 1.2,
|
||
fontWeight: FontWeight.w700, fontVariations: _w700,
|
||
letterSpacing: -0.25,
|
||
);
|
||
|
||
/// The text-only action under a primary button ("Back to home").
|
||
static const textButton = TextStyle(
|
||
fontFamily: _f, fontSize: 14.5, height: 1.2,
|
||
fontWeight: FontWeight.w600, fontVariations: _w600,
|
||
color: DmColors.ink2,
|
||
);
|
||
|
||
/// Uppercase micro-label above a group — the design's `micro`. Crimson when
|
||
/// it heads brand content, [DmColors.ink3] when it heads a neutral group.
|
||
static const eyebrow = TextStyle(
|
||
fontFamily: _f, fontSize: 10.5, height: 1.3,
|
||
fontWeight: FontWeight.w600, fontVariations: _w600,
|
||
letterSpacing: 1.16, color: DmColors.ink3,
|
||
);
|
||
|
||
/// The eyebrow as it appears on the crimson header.
|
||
static const headerEyebrow = TextStyle(
|
||
fontFamily: _f, fontSize: 11.5, height: 1.3,
|
||
fontWeight: FontWeight.w700, fontVariations: _w700,
|
||
letterSpacing: 1.84, color: DmColors.onBrand3,
|
||
);
|
||
|
||
/// Sentence-case heading above a group of rows.
|
||
static const sectionHead = TextStyle(
|
||
fontFamily: _f, fontSize: 15, height: 1.3,
|
||
fontWeight: FontWeight.w700, fontVariations: _w700,
|
||
letterSpacing: -0.3, color: DmColors.ink,
|
||
);
|
||
|
||
/// Alias kept for existing call sites.
|
||
static const groupHead = sectionHead;
|
||
|
||
/// Field input text.
|
||
static const field = TextStyle(
|
||
fontFamily: _f, fontSize: 16, height: 1.3,
|
||
fontWeight: FontWeight.w600, fontVariations: _w600,
|
||
color: DmColors.ink,
|
||
);
|
||
|
||
// ── Mono: anything checkable digit by digit ──────────────────────────────
|
||
|
||
/// References, plates and phone numbers sitting inline in a row.
|
||
static const mono = TextStyle(
|
||
fontFamily: _mono, fontSize: 13, height: 1.4,
|
||
fontWeight: FontWeight.w500, fontVariations: _w500,
|
||
letterSpacing: -0.26, color: DmColors.ink3,
|
||
);
|
||
|
||
/// Step counters, timers, the smallest data.
|
||
static const monoSm = TextStyle(
|
||
fontFamily: _mono, fontSize: 11.5, height: 1.3,
|
||
fontWeight: FontWeight.w500, fontVariations: _w500,
|
||
letterSpacing: -0.23, color: DmColors.ink3,
|
||
);
|
||
|
||
/// A booking reference given its own line, or a fare.
|
||
static const monoLg = TextStyle(
|
||
fontFamily: _mono, fontSize: 19, height: 1.2,
|
||
fontWeight: FontWeight.w500, fontVariations: _w500,
|
||
letterSpacing: -0.38, color: DmColors.ink,
|
||
);
|
||
|
||
/// The OTP digits.
|
||
static const otp = TextStyle(
|
||
fontFamily: _f, fontSize: 24, height: 1.1,
|
||
fontWeight: FontWeight.w600, fontVariations: _w600,
|
||
color: DmColors.ink,
|
||
);
|
||
}
|