Initial commit
This commit is contained in:
613
src/index.css
Normal file
613
src/index.css
Normal file
@@ -0,0 +1,613 @@
|
||||
/* ── Layer order ─────────────────────────────────────────────────────────────
|
||||
Prescribed verbatim by the Usage block at the top of
|
||||
@astryxdesign/core/tailwind-theme.css — not our invention. The ordering
|
||||
matters: `utilities` sits last so Tailwind utilities keep winning over
|
||||
`astryx-base`, which is what makes a half-migrated file behave.
|
||||
|
||||
Note `astryx.css` loads BEFORE the theme package: the theme supplies the
|
||||
token values that the component CSS references. */
|
||||
@layer reset, theme, base, astryx-base, astryx-theme, components, utilities;
|
||||
|
||||
/* Sora for display, Inter for body — the pairing read out of KROW's own
|
||||
stylesheet (--font-display / --font-body). JetBrains Mono stays for the
|
||||
monospace runs: ids, SKUs, PINs, invoice numbers. */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
|
||||
|
||||
@import 'tailwindcss/theme.css' layer(theme);
|
||||
@import 'tailwindcss/preflight.css' layer(base);
|
||||
@import '@astryxdesign/core/reset.css';
|
||||
@import '@astryxdesign/core/astryx.css';
|
||||
/* Generated by `npx astryx theme build src/theme/nearle.ts` — do not hand-edit. */
|
||||
@import './theme/nearle.css';
|
||||
@import '@astryxdesign/core/tailwind-theme.css';
|
||||
@import 'tailwindcss/utilities.css' layer(utilities);
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display: 'Sora', ui-sans-serif, system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
|
||||
|
||||
/* ── Brand ──────────────────────────────────────────────────────────────
|
||||
#662582 is the Nearle purple, taken from the app mark. It scores 9.76:1
|
||||
on white. `--color-brand-strong` is the pressed/hover step. */
|
||||
--color-brand: #662582;
|
||||
--color-brand-strong: #531b6e;
|
||||
--color-brand-soft: #f2ecf6;
|
||||
/* The tint behind an active nav item and an unread row. KROW's
|
||||
`krow-blue-tint`, restated in purple. */
|
||||
--color-brand-tint: #f4eef8;
|
||||
|
||||
/* The 4-step ink scale every component reads from.
|
||||
ink-1 headings/values · ink-2 body · ink-3 secondary · ink-4 meta */
|
||||
--color-ink-1: #0f172a;
|
||||
--color-ink-2: #414b5a;
|
||||
--color-ink-3: #657081;
|
||||
--color-ink-4: #97a1b0;
|
||||
|
||||
/* Surfaces. `subtle` sits under a resting input, `sunken` under a hover. */
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-subtle: #fafbfc;
|
||||
--color-surface-sunken: #f2f4f7;
|
||||
--color-line: #e0e4ea;
|
||||
|
||||
/* Neutral ramp — the ink scale every surface and label reads from. */
|
||||
--color-slate-50: #f7f7f7;
|
||||
--color-slate-100: #eff1f4;
|
||||
--color-slate-200: #e1e5ea;
|
||||
--color-slate-300: #cbd2db;
|
||||
--color-slate-400: #97a1b0;
|
||||
--color-slate-500: #657081;
|
||||
--color-slate-600: #4a5464;
|
||||
--color-slate-900: #0f172a;
|
||||
|
||||
/* Ambient canvas stops — see body::before below. */
|
||||
--color-canvas-violet: #f2ecf6;
|
||||
--color-canvas-mid: #f8f8f8;
|
||||
--color-canvas-warm: #f8f6f1;
|
||||
|
||||
--container-page: 80rem;
|
||||
--container-wide: 108rem;
|
||||
--container-admin: 102rem;
|
||||
}
|
||||
|
||||
/* ── The ambient canvas ──────────────────────────────────────────────────────
|
||||
KROW's single most distinctive visual trait, restated in Nearle purple: a
|
||||
fixed, viewport-sized horizontal gradient painted behind the entire app.
|
||||
|
||||
Three implementation notes, each of which is a design rule in its own right:
|
||||
|
||||
- Fixed, not on `body` — the gradient runs left-to-right and must be sized
|
||||
to the VIEWPORT, not the document, or it stretches on long pages.
|
||||
- `z-index: -1` — it never enters layout, so no shell needs a wrapper.
|
||||
- The stops sit close together in lightness on purpose. The result should
|
||||
read as *softer*, not as *there is a gradient*. */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-canvas-violet) 0%,
|
||||
#f5f2f7 28%,
|
||||
#f7f6f7 45%,
|
||||
var(--color-canvas-mid) 65%,
|
||||
#f8f7f4 86%,
|
||||
var(--color-canvas-warm) 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Every application shell is transparent so the canvas reads through. */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Focus is never the browser default. Declared once, globally, rather than
|
||||
per component. */
|
||||
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
|
||||
outline: 2px solid var(--color-brand);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Reduced motion handled once, globally. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Numeric columns line up only with tabular figures. Used by every KPI value
|
||||
and every money/count cell in a table. */
|
||||
.tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* The hamburger and the mobile sheet only exist below the tabs breakpoint;
|
||||
above it the text tabs are visible and a second nav affordance is noise. */
|
||||
.lg-hidden {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
.lg-hidden {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus treatment for the auth inputs, which are hand-styled rather than
|
||||
Astryx controls — the auth page is the one screen the reference also builds
|
||||
outside the component layer. */
|
||||
input:focus-visible {
|
||||
border-color: var(--color-brand) !important;
|
||||
background: var(--color-surface) !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
RESPONSIVE LAYER
|
||||
═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Breakpoints are Tailwind's stock set, which is what the reference uses — it
|
||||
declares no `screens` key, so the defaults apply:
|
||||
|
||||
base 0–639 phone
|
||||
sm 640 large phone / small tablet
|
||||
md 768 tablet · small laptop
|
||||
lg 1024 laptop
|
||||
xl 1280 small desktop
|
||||
2xl 1536 large desktop
|
||||
|
||||
Mobile-first throughout: the base rule is the phone state and every override
|
||||
is a min-width query. Written as classes rather than inline styles because a
|
||||
media query cannot live in a style attribute, and because layout that depends
|
||||
on JS measuring the viewport flashes the wrong thing on first paint.
|
||||
═════════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Gutters ───────────────────────────────────────────────────────────────
|
||||
16px on a phone, 24px from lg.
|
||||
|
||||
The shell is fluid up to `--container-admin` (1632px of content) and then
|
||||
stops and centres. Fluid-forever was wrong: on a 2560px monitor it stretched
|
||||
a six-column table to 1660px, opening a dead 600px gap between "Tenant" and
|
||||
"City" and stranding three KPI tiles at the far left of a 2100px row. A row
|
||||
the eye cannot track from label to value is not more information, it is less.
|
||||
|
||||
The cap lands on BOTH the header row and the body, because they share this
|
||||
class — nav, page title and table all stay on the same left edge at every
|
||||
width. `calc(... + 48px)` is the content cap plus the two gutters, so the
|
||||
measured content box is exactly `--container-admin`. */
|
||||
.app-gutter {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
max-width: calc(var(--container-admin) + 32px);
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.app-gutter {
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
max-width: calc(var(--container-admin) + 48px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Visibility ─────────────────────────────────────────────────────────────
|
||||
`display` is set here rather than inline, so an inline style cannot win over
|
||||
the query. Each class names the breakpoint it turns on at. */
|
||||
.show-from-md {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.show-from-md {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.show-from-lg {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.show-from-lg {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.lg-hidden {
|
||||
display: inline-flex;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.lg-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Admin body ────────────────────────────────────────────────────────────
|
||||
A column on a phone so the assistant stacks under the page; a row from md,
|
||||
where a tablet already lays the inline column out acceptably. Only phones
|
||||
stack — that threshold is md (768), not lg, on purpose. */
|
||||
.admin-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.admin-body {
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Assistant column ──────────────────────────────────────────────────────
|
||||
Stacked and fixed-height below md; a sticky 380px column above it, sized so
|
||||
the page beside it still has a usable measure. */
|
||||
.assistant {
|
||||
width: 100%;
|
||||
height: 32rem;
|
||||
padding-top: 0;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.assistant {
|
||||
position: sticky;
|
||||
top: 56px;
|
||||
width: 340px;
|
||||
flex: none;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
height: calc(100dvh - 56px);
|
||||
margin-right: -16px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.assistant {
|
||||
margin-right: -24px;
|
||||
}
|
||||
}
|
||||
/* 380px only once the page beside it can afford it. At 1280 the split was
|
||||
828/380 and the tenant table's last column got clipped mid-word; holding
|
||||
Buddy at 340 until 1536 gives the page the width back where it is scarce and
|
||||
the extra 40px where it is not. */
|
||||
@media (min-width: 1536px) {
|
||||
.assistant {
|
||||
width: 380px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Page header ───────────────────────────────────────────────────────────
|
||||
22px on a phone, 24px from sm. */
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Tables ────────────────────────────────────────────────────────────────
|
||||
A table is the one thing that genuinely cannot reflow: columns have meaning
|
||||
and dropping them silently loses data. So it scrolls sideways inside its own
|
||||
container, and the page never does. */
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
/* A scroll shadow, so a clipped column reads as "there is more this way"
|
||||
rather than as a rendering bug. The two `local` layers are opaque covers
|
||||
that travel WITH the content; the two `scroll` layers are the shadows
|
||||
pinned to the container. When the content is flush to an edge its cover
|
||||
sits over that shadow and hides it, so the hint appears only on the side
|
||||
that actually has more to show — no JS, no scroll listener. */
|
||||
background:
|
||||
linear-gradient(to right, var(--color-surface) 30%, rgb(255 255 255 / 0)) left center / 28px 100%
|
||||
no-repeat,
|
||||
linear-gradient(to left, var(--color-surface) 30%, rgb(255 255 255 / 0)) right center / 28px 100%
|
||||
no-repeat,
|
||||
radial-gradient(farthest-side at 0 50%, rgb(16 24 32 / 0.1), rgb(16 24 32 / 0)) left center /
|
||||
12px 100% no-repeat,
|
||||
radial-gradient(farthest-side at 100% 50%, rgb(16 24 32 / 0.1), rgb(16 24 32 / 0)) right center /
|
||||
12px 100% no-repeat;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
.table-scroll > table {
|
||||
min-width: 720px;
|
||||
}
|
||||
|
||||
/* ── Toolbars and filter rows ──────────────────────────────────────────────
|
||||
Stacked on a phone so a search field gets a full row, inline from sm. */
|
||||
.toolbar-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.toolbar-stack {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Login ─────────────────────────────────────────────────────────────────
|
||||
One column below md, two from md — the reference splits at exactly that
|
||||
point. Padding relaxes at sm so a phone is not spending 80px on margins. */
|
||||
.login-shell {
|
||||
padding: 12px;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.login-shell {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-split {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.login-split {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
padding: 24px;
|
||||
border-bottom: 1px solid var(--color-line);
|
||||
border-right: 0;
|
||||
gap: 20px;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.login-brand {
|
||||
padding: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.login-brand {
|
||||
padding: 40px;
|
||||
gap: 32px;
|
||||
border-bottom: 0;
|
||||
border-right: 1px solid var(--color-line);
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 24px;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.login-form {
|
||||
padding: 32px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.login-form {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* The hero image earns its space on a laptop; on a phone it is the thing
|
||||
pushing the form below the fold, so it goes. */
|
||||
.login-hero {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.login-hero {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dialogs ───────────────────────────────────────────────────────────────
|
||||
Full-bleed on a phone; the width cap engages from sm. */
|
||||
.dialog-panel {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.dialog-panel {
|
||||
width: 640px;
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch targets: a 32px control is fine with a mouse and small with a thumb,
|
||||
so coarse pointers get the comfortable minimum. */
|
||||
@media (pointer: coarse) {
|
||||
button,
|
||||
[role='button'],
|
||||
a[href] {
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── KPI rows ──────────────────────────────────────────────────────────────
|
||||
Two up on a phone, three from md, then natural-width tiles from lg.
|
||||
|
||||
`auto-fill` rather than `auto-fit` above lg is deliberate: `auto-fit`
|
||||
collapses the empty tracks and stretches three tiles across a 1400px row,
|
||||
which makes a five-word label sit in the middle of a billboard. `auto-fill`
|
||||
keeps the empty tracks, so a tile stays tile-sized however few there are. */
|
||||
.kpi-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.kpi-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Form rows ─────────────────────────────────────────────────────────────
|
||||
One field per row on a phone, two from sm, three from lg. A three-across
|
||||
form on a 768px tablet puts a postcode field at 200px, which is narrower
|
||||
than the value it holds. */
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* Address rows want four short fields, so they get a tighter ladder. */
|
||||
.form-grid-4 {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.form-grid-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Product / catalogue cards ─────────────────────────────────────────────
|
||||
Two up on a phone — a product grid at one card per row reads as a list and
|
||||
loses the visual comparison that makes a grid worth having. */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Container queries for content grids ───────────────────────────────────
|
||||
The reference uses viewport queries throughout, and for the shell that is
|
||||
right. For the grids INSIDE the page it is wrong, and the assistant column is
|
||||
what exposes it: at 768px the viewport says "tablet, two columns please"
|
||||
while the main column is actually 390px wide because a 340px panel is sitting
|
||||
beside it. The result is a two-column form at phone width, with labels
|
||||
wrapping onto three lines.
|
||||
|
||||
A grid does not care how wide the window is. It cares how wide its own
|
||||
container is. So `main` becomes a query container and these grids ask it
|
||||
directly. The viewport rules above stay as the fallback for anything that
|
||||
cannot do container queries; where both apply, these win by cascade order. */
|
||||
main {
|
||||
container-type: inline-size;
|
||||
container-name: page;
|
||||
}
|
||||
|
||||
@container page (min-width: 520px) {
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@container page (min-width: 860px) {
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@container page (max-width: 519px) {
|
||||
.form-grid,
|
||||
.form-grid-4 {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@container page (min-width: 520px) {
|
||||
.form-grid-4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@container page (min-width: 860px) {
|
||||
.form-grid-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@container page (min-width: 380px) {
|
||||
.kpi-grid,
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@container page (min-width: 620px) {
|
||||
.kpi-grid,
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@container page (min-width: 900px) {
|
||||
/* `auto-fit`, not `auto-fill`. `auto-fill` keeps the empty tracks it makes
|
||||
room for, so a four-tile row on a wide screen renders four tiles and a
|
||||
ragged hole where a fifth would have gone. `auto-fit` collapses those
|
||||
tracks and the tiles divide the row evenly.
|
||||
|
||||
The reason to have preferred `auto-fill` — three tiles stretching across a
|
||||
very wide row — no longer applies: the shell caps content at
|
||||
`--container-admin`, so the widest a page column ever gets is 1228px, and
|
||||
three tiles across that is a legible 400px each rather than an absurd 700. */
|
||||
.kpi-grid,
|
||||
.card-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Terminal cards ────────────────────────────────────────────────────────
|
||||
Its own grid rather than `.card-grid`, because these cards carry sentences,
|
||||
not figures: a chip row, a line of what is actually true, and a line of what
|
||||
to do about it. `.card-grid` opens at two columns even on a phone, which is
|
||||
right for a branch tile of four numbers and wrong here — 180px of width turns
|
||||
"Bills stranded" into four wrapped lines.
|
||||
|
||||
So: one column until there is room for prose, then `auto-fit` on a 280px
|
||||
floor. Against the 1228px content cap that tops out at four across. */
|
||||
.terminal-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
@container page (min-width: 560px) {
|
||||
.terminal-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user