/* tasteskill / app.css A neutral foundation for app screens (dashboards, admin, settings, inboxes), set to the defaults measured in the study of real product screens (references/app-screens.md). Copy it into the project, replace the token values with the ones on the app sheet (draw.mjs --app), and build on top. Delete what you do not use. It has no brand of its own: neutral grounds, one quiet line, one accent. */ /* ---------- tokens (replace with the dealt style) ---------- */ :root { /* names match the app sheet printed by draw.mjs --app */ --ground: #f9fafb; /* the app chrome and page */ --surface: #ffffff; /* panels and the work sheet */ --raised: #f4f5f6; /* a filled inner well, hover rows */ --active: #eeeeee; /* the active nav pill */ --border: #e8e8e8; /* panel edges: about 1.2:1 on white */ --rule: #f1f2f3; /* row rules inside tables */ --control: #d1d5db; /* inputs and outlined buttons: one step stronger */ --text: #111111; --muted: #5f6368; /* secondary text, labels (4.5:1 or more on its ground) */ --faint: #6b6b6b; /* meta and placeholders: the lightest grey that holds 4.5:1 on white */ --accent: #1f6f5c; /* one hue: data, focus, the selected thing */ --primary: #1d1f20; /* the primary button is near-black unless the style says otherwise */ --primary-ink: #ffffff; --positive: #247b50; --negative: #c4242a; --warning: #92400e; /* references/app-screens.md ยง9.5; a green accent is also --positive */ --pill-positive-bg: #dcfae5; --pill-positive-ink: #04250e; --pill-negative-bg: #ffedea; --pill-negative-ink: #831c1e; --pill-warning-bg: #fffbeb; --pill-warning-ink: #92400e; --pill-neutral-bg: #eeeeee; --pill-neutral-ink: #3d3d3d; --chart-1: var(--accent); --chart-2: #8fb8ad; --chart-3: #b08a2e; --chart-muted: #cfd3d6; --chart-mark: var(--text); /* a marked period in Charts.diverging: the second tone */ --chart-span: 0.06; /* opacity of a shaded span (a selling window, an outage) */ --grid: var(--rule); /* chart gridlines (charts.js) */ --axis-ink: var(--muted); /* chart tick labels (charts.js) */ --ink: var(--text); --font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; --fs-title: 24px; --fs-section: 16px; --fs-kpi: 28px; --fs-body: 14px; --fs-small: 12px; --sidebar-width: 232px; --pad: 24px; /* canvas padding */ --gap: 16px; /* one gutter, horizontal and vertical */ --card-pad: 20px; --radius-card: 8px; /* radius is bimodal: tight 4โ€“8 (controls 6) or soft 16โ€“24 (controls full); never 11โ€“12 */ --radius-control: 6px; --control-height: 32px; /* every control in the title and filter rows */ --row-height: 48px; /* single-line table row */ --nav-row: 32px; /* sidebar item height, on a 34px pitch */ --ease-out: cubic-bezier(0.22, 1, 0.36, 1); --t-press: 120ms; --t-hover: 150ms; --t-ui: 200ms; } /* dark: one true-neutral ground, elevation by lightness; two greys that read apart (not #aaa beside #969696) */ :root[data-theme="dark"] { --ground: #000000; --surface: #111111; --raised: #222222; --active: #222222; --border: #1a1a1a; --rule: #161616; --control: #444444; --text: #ffffff; --muted: #b4b4b4; --faint: #949494; --primary: #ffffff; --primary-ink: #000000; --positive: #3fc07a; --negative: #e5566f; --warning: #e5a93b; /* a green or yellow-green accent is also --positive */ --pill-positive-bg: #172a1d; --pill-positive-ink: #3fc07a; --pill-negative-bg: #2c1a20; --pill-negative-ink: #e5566f; --pill-warning-bg: #2b2414; --pill-warning-ink: #e5a93b; --pill-neutral-bg: #222222; --pill-neutral-ink: #ffffff; --chart-muted: #4a4a4a; --grid: #2b2b2b; --chart-span: 0.12; } /* ---------- reset ---------- */ *, *::before, *::after { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; } body { margin: 0; background: var(--ground); color: var(--text); font: var(--fs-body)/20px var(--font); -webkit-font-smoothing: antialiased; } h1, h2, h3, h4, p, figure, ul, ol, dl, dd { margin: 0; } ul[class], ol[class] { padding: 0; list-style: none; } img, svg, video, canvas { display: block; max-width: 100%; } button, input, select, textarea { font: inherit; color: inherit; } a { color: inherit; text-decoration: none; } :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; } .num, table, .kpi-value { font-variant-numeric: tabular-nums; } /* ---------- shell: sidebar + canvas ---------- */ .app { display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); min-height: 100svh; } .sidebar { position: sticky; top: 0; height: 100svh; display: flex; flex-direction: column; gap: 2px; padding: 16px 12px; overflow: auto; } .sidebar .group-label { margin: 20px 8px 6px; font-size: var(--fs-small); color: var(--faint); } .nav-item { display: flex; align-items: center; gap: 10px; height: var(--nav-row); padding: 0 8px; border-radius: 8px; color: var(--muted); transition: background-color var(--t-hover) ease, color var(--t-hover) ease; } .nav-item svg { width: 16px; height: 16px; flex: none; stroke-width: 1.5; } .nav-item:hover { background: var(--raised); color: var(--text); } .nav-item[aria-current="page"] { background: var(--active); color: var(--text); } .sidebar .bottom { margin-top: auto; } .canvas { min-width: 0; padding: var(--pad); } /* ---------- title block and filters ---------- */ .title-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 20px; } .title-row h1 { font-size: var(--fs-title); line-height: 1.2; font-weight: 600; letter-spacing: -0.01em; } .title-row .sub { margin-top: 4px; color: var(--muted); } .filters { display: flex; align-items: center; gap: 8px; margin-bottom: var(--gap); flex-wrap: wrap; } .filters .spacer { flex: 1; } .stamp { color: var(--faint); font-size: var(--fs-small); } /* ---------- panels and grid ---------- */ .grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--gap); } .panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card); padding: var(--card-pad); min-width: 0; } .panel > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; } .panel h2 { font-size: var(--fs-section); line-height: 1.3; font-weight: 600; } .panel .meta { color: var(--faint); font-size: var(--fs-small); } /* paired cards in one row: a card stretches to its neighbour only when it has content to fill. Otherwise it keeps its own height (add .top to the row), and only an action or a total may be pinned to a stretched card's bottom */ .grid.top { align-items: start; } /* a chart's heading row: its title (and legend or period) with a fixed 8px to the svg */ .chart-head { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: 4px 12px; margin-bottom: 8px; } .chart-head h3 { font-size: var(--fs-body); line-height: 20px; font-weight: 500; } .chart { position: relative; min-width: 0; } /* a link at a card's foot or header ("Show all 7", "Sell-out history"): ink, a 12px chevron, an underline on hover, and no inline padding, so its text sits on the card's content edge */ .card-link { display: inline-flex; align-items: center; gap: 2px; padding: 0; border: 0; background: none; color: var(--text); font-weight: 500; cursor: pointer; text-underline-offset: 3px; } .card-link::after { content: ""; width: 12px; height: 12px; flex: none; background: currentColor; -webkit-mask: var(--chevron) center / 12px no-repeat; mask: var(--chevron) center / 12px no-repeat; } .card-link:hover { text-decoration: underline; } :root { --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M4.5 2.5 8 6l-3.5 3.5' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); } /* list rows: a rule between rows and none above the first. A rule on a rounded box curls up at both ends, so a row that carries a rule has square corners */ .rows { display: grid; } .row { display: flex; align-items: center; gap: 12px; min-height: 44px; padding: 10px 0; } .row + .row { border-top: 1px solid var(--rule); border-radius: 0; } /* KPIs as one divided strip: a label above a number, no icons, no tiles */ .kpis { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-card); } .kpi { padding: 16px var(--card-pad); } .kpi + .kpi { border-left: 1px solid var(--border); } .kpi-label { color: var(--muted); } .kpi-value { margin-top: 6px; font-size: var(--fs-kpi); line-height: 1.15; font-weight: 500; letter-spacing: -0.01em; } .kpi-value small { font-size: 0.55em; color: var(--muted); font-weight: 400; margin-left: 2px; } .delta { margin-top: 4px; font-size: var(--fs-small); color: var(--muted); } .delta .up { color: var(--positive); } .delta .down { color: var(--negative); } /* KPIs as the chart's tabs: the selected one carries a 2px rule and an ink label; the others keep a grey label and their value in ink at a lighter weight (a grey value reads as disabled). When the tabs are the per-entity breakdown the brief asks for (takings by shop), add .entities: nothing changes but the rule */ .kpis[role="tablist"] .kpi { position: relative; cursor: pointer; } .kpis[role="tablist"] .kpi[aria-selected="true"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--text); } .kpis[role="tablist"]:not(.entities) .kpi[aria-selected="true"] .kpi-label { color: var(--text); } .kpis[role="tablist"]:not(.entities) .kpi[aria-selected="false"] .kpi-value { color: var(--text); font-weight: 400; } /* ---------- tables ---------- */ .table-wrap { overflow-x: auto; } table { width: 100%; border-collapse: collapse; } th { height: 36px; padding: 0 12px; text-align: left; font-weight: 400; font-size: var(--fs-small); color: var(--muted); border-bottom: 1px solid var(--border); white-space: nowrap; } /* cells share one baseline, so a figure never sits 2px below the name beside it; the row height comes from padding */ td { padding: calc((var(--row-height) - 20px) / 2) 12px; border-bottom: 1px solid var(--rule); vertical-align: baseline; } tbody tr:last-child td { border-bottom: 0; } /* no hover that changes the fill; a row that drives a detail pane takes the nav pill's fill when selected */ tbody tr[aria-selected="true"] td { background: var(--active); } th.n, td.n { text-align: right; } td .sub { display: block; font-size: var(--fs-small); color: var(--muted); } .truncate { max-width: 28ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* status pill: a pale tint with dark ink of the same hue, one per row */ .pill { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: 999px; font-size: var(--fs-small); font-weight: 500; background: var(--pill-neutral-bg); color: var(--pill-neutral-ink); white-space: nowrap; } .pill.pos { background: var(--pill-positive-bg); color: var(--pill-positive-ink); } .pill.neg { background: var(--pill-negative-bg); color: var(--pill-negative-ink); } .pill.warn { background: var(--pill-warning-bg); color: var(--pill-warning-ink); } /* ---------- controls ---------- */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: var(--control-height); padding: 0 12px; border-radius: var(--radius-control); border: 1px solid var(--control); background: var(--surface); font-weight: 500; cursor: pointer; transition: background-color var(--t-hover) ease, transform var(--t-press) var(--ease-out); } .btn:hover { background: var(--raised); } .btn:active { transform: translateY(1px); } .btn.primary { background: var(--primary); color: var(--primary-ink); border-color: transparent; } .btn.ghost { border-color: transparent; background: transparent; } .field { display: grid; gap: 8px; } .field label { font-weight: 500; } .field .hint { color: var(--muted); font-size: var(--fs-small); } .input, select.input { height: 38px; padding: 0 12px; border: 1px solid var(--control); border-radius: var(--radius-control); background: var(--surface); } .input::placeholder { color: var(--faint); } .tabs { display: flex; gap: 24px; border-bottom: 1px solid var(--border); margin-bottom: var(--gap); } .tabs a { position: relative; padding: 10px 0; color: var(--muted); transition: color var(--t-hover) ease; } .tabs a[aria-selected="true"] { color: var(--text); } .tabs a[aria-selected="true"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--text); } /* as wide as its options; full width only on a narrow phone */ .segmented { display: inline-flex; width: max-content; max-width: 100%; padding: 2px; border-radius: 8px; background: var(--raised); } .segmented button { height: 28px; padding: 0 10px; border: 0; border-radius: 6px; background: transparent; color: var(--muted); cursor: pointer; } .segmented button[aria-pressed="true"] { background: var(--surface); color: var(--text); } .switch { appearance: none; width: 32px; height: 20px; border-radius: 999px; background: var(--control); position: relative; cursor: pointer; transition: background-color var(--t-ui) ease; } .switch::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform var(--t-ui) var(--ease-out); } .switch:checked { background: var(--text); } .switch:checked::after { transform: translateX(12px); } /* empty and loading states keep the frame; a loading value is a flat grey bar at its real size, with no shimmer */ .empty { display: grid; place-items: center; min-height: 96px; padding: 16px; border: 1px dashed var(--control); border-radius: var(--radius-control); color: var(--muted); text-align: center; } .skeleton { background: var(--raised); border-radius: 4px; } /* ---------- phone: the navigation folds away and the canvas takes the width ---------- */ .menu-toggle { display: none; } @media (max-width: 860px) { .app { grid-template-columns: minmax(0, 1fr); } /* the menu opens as a full-screen sheet */ .sidebar { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 20; background: var(--surface); transform: translateX(-100%); transition: transform var(--t-ui) var(--ease-out); } .sidebar.open { transform: none; } .menu-toggle { display: inline-flex; } .canvas { padding: 16px; } .grid > * { grid-column: 1 / -1 !important; } .kpis { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); } .kpi + .kpi { border-left: 0; } .kpi:nth-child(n + 3) { border-top: 1px solid var(--border); } .kpi:nth-child(2n) { border-left: 1px solid var(--border); } /* an odd count leaves no half-empty row: the last cell takes the full width */ .kpi:last-child:nth-child(odd) { grid-column: 1 / -1; } /* KPI tabs in a 2-up grid: the selected cell takes a fill, never an underline floating mid-grid */ .kpis[role="tablist"] .kpi[aria-selected="true"] { background: var(--active); } .kpis[role="tablist"] .kpi[aria-selected="true"]::after { content: none; } .title-row { flex-wrap: wrap; align-items: flex-start; } } @media (max-width: 480px) { .segmented { display: flex; width: 100%; } .segmented button { flex: 1; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation: none !important; } } /* an empty message spans the block it stands in, never one squeezed column */ .rows .empty, .grid > .empty, li > .empty { grid-column: 1 / -1; width: 100%; }