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); /// 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. /// /// 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); 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); /// 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, ); }