Files
threadcount-community/app/globals.css
T
ThreadCount 344b1701dd ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
2026-09-13 08:54:35 +10:00

502 lines
33 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ThreadCount — Modernist design system tokens (authored from the handoff spec) */
:root {
--color-bg: #f3f2f2;
--color-surface: #eae9e9;
--color-text: #201e1d;
--color-accent: #ec3013;
--color-accent-300: #ffc4b8;
/* The brand red carries white type on every primary button, accent tag and error bar, and at
#ec3013 that pairing is 4.20:1 — under the 4.5:1 a label at body size needs. 600 is the same
red one step down: white on it is 5.07:1, and it is 4.54:1 as text on the paper ground, so it
is the shade to fill anything that has words on it. #ec3013 stays the brand mark and belongs on
the rules, the progress bar and the 4px edge marks, which carry no text and need only 3:1. */
--color-accent-600: #d42a12;
--color-accent-700: #b8240e;
--color-divider: #cfcccb;
--color-neutral-200: #e4e2e1;
--color-neutral-300: #d6d3d2;
/* 400 is only legible on ink — it is the muted meta colour inside the dark panels and top bars
(7.8:1 there). On the paper ground it is 1.9:1, so anything that has to be read on the light
side wants 600 or 700 instead. */
--color-neutral-400: #b5b1af;
--color-neutral-500: #928d8a;
--color-neutral-900: #2d2b2b;
/* Was #7a7573, which is 4.07:1 on the paper ground and 3.75:1 on the surface tone — under the
minimum for the sub-lines, table headers and tab labels this colour carries. */
--color-neutral-600: #6c6764;
--color-neutral-700: #57534f;
--color-neutral-800: #3a3735;
--color-slip-teal: #9acbd8;
/* The desktop app's chrome, and only the desktop app's. #201e1d, #f3f2f2, #ec3013 and #b8240e
are the four the Android shell already ships, so nothing here is a new colour in the product;
#2b2827 is one step up from the ink so the rail reads as lifted off the page header rather
than welded to it, and #37332f is the lightest hairline that still shows on ink. These are
literal values on purpose: the desktop scopes below remap some --color-* tokens on the ink
bands, and a --tc-* token defined as var(--color-…) would be remapped along with them. */
--tc-ink: #201e1d;
--tc-rail: #2b2827;
--tc-hair: #37332f;
--tc-on-ink: #f3f2f2;
--tc-ink-idle: #d6d3d2;
--tc-ink-muted: #b5b1af;
--tc-on-ink-accent: #ffc4b8;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--font-heading: var(--font-archivo), "Archivo", system-ui, sans-serif;
--font-body: var(--font-archivo), "Archivo", system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--color-bg); color: var(--color-text); font-family: var(--font-body); font-size: 14px; line-height: 1.4; -webkit-font-smoothing: antialiased; }
/* Chrome's text autosizing "boosts" font sizes in the Android WebView — a 15px top-bar title came
out at 24px on a real phone, which is why the app's chrome looked oversized against the design.
The sizes here are deliberate, so opt out of the boosting; the OS accessibility font scale still
applies on top of them. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
a { color: var(--color-accent-700); }
a:hover { color: var(--color-accent-600); }
h1, h2, h3 { font-family: var(--font-heading); margin: 0; }
b, strong { font-weight: 700; }
button, input, select, textarea { font-family: var(--font-body); font-size: 14px; color: var(--color-text); border-radius: 0; }
input[type="checkbox"] { accent-color: var(--color-text); }
/* Buttons — flush-left labels, zero radius */
.btn {
display: inline-flex; align-items: center; justify-content: flex-start; gap: 6px;
min-height: 36px; padding: 6px 14px; font-weight: 600; font-size: 13px; line-height: 1.2;
border: 2px solid transparent; background: transparent; color: var(--color-text); cursor: pointer;
text-align: left; white-space: nowrap; text-decoration: none;
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary { background: var(--color-accent-600); border-color: var(--color-accent-600); color: #fff; }
.btn-primary:not(:disabled):hover { background: var(--color-accent-700); border-color: var(--color-accent-700); }
.btn-secondary { background: transparent; border-color: var(--color-text); color: var(--color-text); }
.btn-secondary:not(:disabled):hover { background: var(--color-neutral-200); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--color-text); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1.5px; }
.btn-ghost:not(:disabled):hover { background: var(--color-neutral-200); }
.btn-block { width: 100%; }
/* Tags */
.tag {
display: inline-flex; align-items: center; padding: 2px 7px; font-size: 10px; font-weight: 700;
letter-spacing: 0.08em; text-transform: uppercase; line-height: 1.5; border: 1.5px solid transparent; white-space: nowrap;
}
.tag-accent { background: var(--color-accent-600); border-color: var(--color-accent-600); color: #fff; }
.tag-neutral { background: var(--color-neutral-200); border-color: var(--color-neutral-200); color: var(--color-text); }
.tag-outline { background: transparent; border-color: var(--color-text); color: var(--color-text); }
/* Segmented control */
.seg { display: inline-flex; border: 2px solid var(--color-text); background: var(--color-bg); }
.seg-opt {
padding: 5px 12px; min-height: 30px; font-size: 12px; font-weight: 600; background: transparent;
border: none; border-right: 2px solid var(--color-text); color: var(--color-text); cursor: pointer; white-space: nowrap;
}
.seg-opt:last-child { border-right: none; }
.seg-opt.btn-primary { background: var(--color-accent-600); color: #fff; }
/* Inputs */
.input {
min-height: 36px; padding: 6px 10px; font-size: 14px; border: 2px solid var(--color-text); background: #fff; color: var(--color-text);
outline: none; width: auto; max-width: 100%;
}
.input:focus { border-color: var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent); }
.input:disabled { background: var(--color-surface); color: var(--color-neutral-700); }
select.input { appearance: none; -webkit-appearance: none; padding-right: 28px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1l5 5 5-5' fill='none' stroke='%23201e1d' stroke-width='2'/></svg>"); background-repeat: no-repeat; background-position: right 10px center; }
.field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.field label { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); }
.field .input { width: 100%; }
/* Tables */
.table { border-collapse: collapse; width: 100%; font-size: 13px; }
.table th { font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-neutral-600); border-bottom: 2px solid var(--color-text); padding: 8px 8px 6px; }
.table td { padding: 8px; border-bottom: 1px solid var(--color-divider); vertical-align: middle; }
.table tr:last-child td { border-bottom: 1px solid var(--color-divider); }
/* Section heads */
.sec { border-bottom: 2px solid var(--color-text); padding-bottom: var(--space-2); font-size: 12px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
.eyebrow { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--color-accent-700); }
.h1 { font-family: var(--font-heading); font-weight: 800; font-size: 30px; margin: var(--space-1) 0 0; letter-spacing: -0.02em; }
.page-head { padding: var(--space-6) 0 var(--space-4); border-bottom: 2px solid var(--color-text); display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }
.row-hover:hover { background: var(--color-neutral-200); }
.muted { color: var(--color-neutral-700); }
.num { font-family: var(--font-heading); font-weight: 800; }
/* Dialogs */
.overlay { position: fixed; inset: 0; background: color-mix(in srgb, #201e1d 45%, transparent); display: flex; align-items: center; justify-content: center; z-index: 50; padding: 16px; }
.dialog { background: var(--color-bg); border: 2px solid var(--color-text); padding: var(--space-6); max-height: 88vh; overflow: auto; width: 100%; }
.dialog-title { font-family: var(--font-heading); font-weight: 800; font-size: 20px; }
/* App shell */
/* Skip link: out of the way until it takes focus, then a solid ink chip in the top-left corner.
Fixed rather than in flow so it cannot disturb the shell's flex row while it is hidden. */
.skip-link { position: fixed; top: 8px; left: -9999px; z-index: 100; background: var(--color-text); color: var(--color-bg); border: 2px solid var(--color-text); padding: 10px 14px; font-size: 13px; font-weight: 700; text-decoration: none; }
.skip-link:focus { left: 8px; }
/* The content landmark only holds focus so the skip link can hand it over; a ring around the whole
page would be noise, and nothing else can focus it. */
main.tc-main:focus { outline: none; }
/* --tc-gutter is the page's side margin. The page head bleeds out to it with a negative margin,
so the two have to be one number or the ink band stops short of the edge. */
.tc-shell { display: flex; min-height: 100vh; align-items: stretch; --tc-gutter: var(--space-8); }
#tc-side {
--tc-rail-w: 232px;
width: var(--tc-rail-w); flex: 0 0 var(--tc-rail-w);
background: var(--tc-rail); color: var(--tc-ink-idle);
border-right: 2px solid var(--tc-ink);
display: flex; flex-direction: column;
position: sticky; top: 0; align-self: flex-start; height: 100vh;
/* Eleven items, a footer and a short window: the rail scrolls rather than losing Sign out off
the bottom. Collapsed it must not, because the hover labels are drawn outside its width and
overflow-y clips the other axis with it. */
overflow-y: auto; overflow-x: hidden;
transition: width 140ms ease, flex-basis 140ms ease;
}
#tc-side.tc-rail-narrow { --tc-rail-w: 62px; overflow: visible; }
@media (prefers-reduced-motion: reduce) { #tc-side { transition: none; } }
#tc-mobilebar { display: none; }
main.tc-main { flex: 1; min-width: 0; padding: 0 var(--tc-gutter) var(--space-8); }
#tc-scanfab { display: none; }
@media screen and (max-width: 780px) {
/* main's padding drops to space-3 below; the page head bleeds by the same number or it hangs
over the edge of a narrow window. */
.tc-shell { --tc-gutter: var(--space-3); }
#tc-side { display: none !important; }
#tc-mobilebar { display: grid !important; }
#tc-scanfab { display: flex !important; }
/* The fab sits above the dialog overlay; hide it while a dialog is open so it can't be tapped
through (the scan dialogs have their own Camera button). */
body:has(.overlay) #tc-scanfab { display: none !important; }
main.tc-main { padding: 0 var(--space-3) 110px !important; }
.tc-grid { grid-template-columns: 1fr !important; }
/* A grid track defaults to a min-content floor, so a wide table (the size list on a product
page) stretches its column instead of scrolling inside .table-wrap and drags the whole page
sideways. Let the tracks shrink and the wrapper does its job. */
.tc-grid > * { min-width: 0; }
.dialog { max-width: 94vw !important; }
/* Touch targets: 44px controls, 16px inputs (stops iOS zoom), 40px segmented options, scrollable tables. */
.btn { min-height: 44px; }
.input, select.input { min-height: 44px; font-size: 16px; }
.seg .seg-opt { min-height: 40px; }
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.table { display: block; overflow-x: auto; }
/* Scoped to the desktop app's content area. As a bare `h1 !important` this reached every
heading on every phone-width screen — the counter app's 15px top-bar titles came out at 24px,
which is why the app's chrome looked oversized on a real device, and the marketing hero and
the onboarding headings were flattened to 24px too. Every other rule in this block is scoped
to the desktop shell; this one wasn't. */
main.tc-main h1, main.tc-main .h1 { font-size: 24px !important; }
.page-head .btn { min-height: 44px; padding: 6px 12px; }
}
.table-wrap { overflow-x: auto; }
@media screen and (max-width: 820px) {
.tc-auth { grid-template-columns: 1fr !important; min-height: 100dvh !important; }
.tc-brandpane { display: none !important; }
/* Phone: brand strip on top, form starts high (keyboard-friendly), links home/demo underneath. */
.tc-authpane { align-items: flex-start !important; padding: 20px 20px calc(28px + env(safe-area-inset-bottom)) !important; }
.tc-brandmobile, .tc-authfoot { display: flex !important; }
.tc-auth .seg .seg-opt { min-height: 44px; }
.tc-auth .btn-primary { min-height: 48px; font-size: 15px; }
.mk-grid2, .mk-grid3, .mk-grid4, .mk-hero, .mk-rep { grid-template-columns: 1fr !important; }
.mk-h1 { font-size: 38px !important; }
.tcl-grid { grid-template-columns: 1fr !important; }
.tcl-side { position: static !important; border-right: none !important; border-bottom: 2px solid var(--color-text); display: flex; flex-wrap: wrap; gap: 4px; padding: 12px 16px !important; }
.tcl-side .btn { width: auto !important; }
}
/* The marketing nav is sticky, so an anchor jump lands the heading underneath it. */
#loop, #product, #features, #reporting, #price { scroll-margin-top: 96px; }
@media screen and (max-width: 900px) {
.tcm-hero, .tcm-loop, .tcm-rep, .tcm-price { grid-template-columns: 1fr !important; }
.tcm-feat { grid-template-columns: 1fr 1fr !important; }
.tcm-stats { grid-template-columns: 1fr 1fr !important; }
.tcm-foot { grid-template-columns: 1fr 1fr !important; }
.tcm-rail { display: none !important; }
.tcm-navlinks { gap: 16px !important; font-size: 12.5px !important; }
.tcm-hero > div:first-child { border-right: none !important; border-bottom: 2px solid var(--color-text); }
#reporting > div:first-child { border-right: none !important; border-bottom: 2px solid var(--color-text); }
.tcm-price > div:first-child { border-right: none !important; border-bottom: 1px solid var(--color-divider); }
}
@media screen and (max-width: 820px) {
.tcm-foot { grid-template-columns: 1fr 1fr !important; }
}
@media screen and (max-width: 780px) {
/* Handoff hides the desktop link row here. It leaves a second scrollable row in its place so the
pages stay reachable — the handoff flags "no mobile menu was designed" as an open question. */
.tcm-navlinks, .tcm-navlogin { display: none !important; }
.tcm-navmobile { display: block !important; }
}
@media screen and (max-width: 900px) {
.tcm-headsplit, .tcm-split, .tcm-3col, .tcm-2col { grid-template-columns: 1fr !important; }
/* The left cell of a .tcm-split carries no left padding of its own — on a wide screen it sits on
the page gutter, which the wrap has already paid for. Collapsed to one column that zero becomes
copy printed against the edge of the phone, so the gutter goes back here. */
.tcm-splitpad { padding-left: 40px; }
/* A grid track floors at min-content, so a fixed-width mock inside a collapsed split stretches
its column and drags the whole page sideways instead of scrolling inside its own .table-wrap. */
.tcm-split > * { min-width: 0; }
.tcm-rowgrid { grid-template-columns: 1fr !important; gap: 6px !important; }
.tcm-pullup { margin-top: 0 !important; }
.tcm-stagger > * { padding-top: 0 !important; }
}
@media screen and (max-width: 560px) {
.tcm-pillars { grid-template-columns: 1fr !important; }
}
@media screen and (max-width: 640px) {
/* Phones: drop the in-page links so the sticky bar stays one row and keeps the CTAs above the fold. */
.tcm-navlinks { display: none !important; }
.tcm-nav { padding: 12px 16px !important; gap: 12px !important; }
#loop, #product, #features, #reporting, #price { scroll-margin-top: 72px; }
/* The hero's second and third mock cards are illustration. On a phone they added ~490px
of scrolling before the first real section, so show one and get on with it. */
.tcm-mock-extra { display: none !important; }
.tcm-mock-first { margin-bottom: 20px !important; }
}
@media screen and (max-width: 560px) {
.tcm-feat, .tcm-foot { grid-template-columns: 1fr !important; }
.tcm-feat-lead { grid-column: auto !important; }
}
@media print {
#tc-side, #tc-mobilebar, #tc-scanfab, .skip-link, .no-print { display: none !important; }
main.tc-main { padding: 0 !important; }
body { background: #fff; }
}
/* Update wave additions */
.btn-icon { padding: 0 8px; min-height: 26px; text-decoration: none; font-size: 16px; line-height: 1; }
.kpi-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 2px; background: var(--color-divider); border: 2px solid var(--color-text); margin-top: var(--space-4); }
.kpi-strip > div { background: var(--color-bg); padding: var(--space-3); }
.kpi-strip .kv { font-family: var(--font-heading); font-weight: 800; font-size: 24px; }
.kpi-strip .kl { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); font-weight: 600; margin-top: 2px; }
.link-name { cursor: pointer; border-bottom: 2px solid var(--color-accent); color: inherit; text-decoration: none; }
.link-name:hover { color: var(--color-accent-700); }
.notice { border-left: 4px solid var(--color-accent); padding: var(--space-2) var(--space-3); font-size: 13px; font-weight: 600; margin-bottom: var(--space-3); }
.bar-track { height: 10px; border: 2px solid var(--color-text); }
.bar-fill { height: 100%; background: var(--color-text); }
textarea.input { font-family: var(--font-body); resize: vertical; }
@media screen and (max-width: 780px) { .kpi-strip { grid-template-columns: 1fr 1fr; } }
/* Marketing photography: every image is greyscale, never tinted or restored on hover. */
.grayscale { filter: grayscale(1) contrast(1.08); }
/* ---------- the phone app (/m): a fixed-height column, only the body scrolls */
/* The page returns a fragment, so its top bar, rule, body and nav are this column's own children. */
.tcx-app { position: fixed; inset: 0; display: flex; flex-direction: column; background: var(--color-bg); overflow: hidden; }
/* Android 15 draws apps edge to edge whether they ask or not, so the ink top bar would otherwise
sit under the status bar with the screen title behind the clock. The inset is padding on the bar
itself, so the ink still runs to the top of the screen — the colour bleeds, the words don't. */
.tcx-topbar { padding-top: env(safe-area-inset-top, 0px); height: calc(56px + env(safe-area-inset-top, 0px)) !important; flex-basis: calc(56px + env(safe-area-inset-top, 0px)) !important; }
.tcx-bar:not(:disabled):active { filter: brightness(0.86); }
@keyframes tcx-sweep { 0%, 100% { top: 6px; } 50% { top: calc(100% - 9px); } }
.tcx-laser { animation: tcx-sweep 1.6s ease-in-out infinite; top: 6px; }
@media (prefers-reduced-motion: reduce) { .tcx-laser { animation: none; top: 50%; } }
/* Bars on the counting screen carry the chart in "variance over time" */
.tcx-chart { display: flex; align-items: flex-end; gap: 5px; height: 56px; border-bottom: 1px solid var(--color-divider); }
.tcx-chart > i { flex: 1; min-width: 6px; display: block; }
/* Native scanning: MLKit draws the camera preview behind the WebView, so the page has to get
out of the way while it runs. Only ever set inside the Android shell. */
html.tcx-native-scan, html.tcx-native-scan body, html.tcx-native-scan .tcx-app { background: transparent !important; }
/* Everything the shelf screen drew goes; the scan overlay stays. `.tcx-scanui` is the class the
camera overlay's own root carries (components/MScan.tsx) — if nothing on the page carries it,
this hides the scan UI as well, and a coordinator mid-count is left with a bare camera picture:
no LIVE/PAUSED header, no running counted/expected figure and no "Stop scanning" bar to press,
only the hardware back button, which walks out of the count altogether. */
html.tcx-native-scan .tcx-app > *:not(.tcx-scanui) { visibility: hidden; }
/* The overlay is drawn for the browser scanner, where the picture comes from its own <video>.
Under MLKit the picture is behind the WebView, so the overlay's ground and its camera window —
the last <div> in it, the one wrapping the video — have to let it through, or the spared UI
covers the preview with flat ink and the garment can't be aimed at. The header, the last-scans
panel and the bottom bar keep their own backgrounds so they stay readable over the picture. */
html.tcx-native-scan .tcx-scanui,
html.tcx-native-scan .tcx-scanui .tcx-camwin { background: transparent !important; }
/* The browser scanner's <video> is still in the camera window under MLKit, with no stream to
show. Android's WebView paints a source-less video as an opaque grey box with a play glyph in
the middle — which is exactly what a coordinator in hands-free mode saw on a Pixel 8 Pro on
2026-09-12 instead of the shelf: the native preview was behind it the whole time. */
html.tcx-native-scan .tcx-scanui video { display: none !important; }
/* On a tablet the app keeps a phone-shaped column rather than stretching a list row to 1200px —
a row that wide puts the garment name and its count at opposite ends of the screen. */
@media (min-width: 620px) {
.tcx-app { align-items: center; }
.tcx-app > * { width: 100%; max-width: 560px; }
.tcx-app { background: var(--color-surface); }
}
/* ---------- the desktop app (/app): dark chrome, paper content
*
* Everything that changes how an existing thing looks, from here down, is scoped to .tc-shell —
* the wrapper components/Shell.tsx draws and nothing else in the product draws. The counter app
* (/m) and the staff app (/my) build their screens out of the --color-* tokens at the top of this
* file and the .tcx-* classes above, and neither ever renders a .tc-shell, so none of those
* selectors can reach a phone. The unscoped rules below add new class names only — nothing the
* marketing site or the two phone apps already wears.
*
* The chrome went dark and the content did not. A shelf count is read across a linen room under
* ward lighting, and paper holds its contrast there in a way an ink panel does not. */
.tc-rail-brand {
display: flex; align-items: center; gap: var(--space-2);
padding: var(--space-5) var(--space-4) var(--space-3);
}
.tc-rail-mark { width: 14px; height: 14px; flex: 0 0 14px; background: var(--color-accent); }
.tc-rail-word { font-family: var(--font-heading); font-weight: 800; font-size: 18px; letter-spacing: -0.02em; color: var(--tc-on-ink); flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; }
.tc-rail-facility {
padding: 0 var(--space-4) var(--space-3);
font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--tc-ink-muted);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
border-bottom: 1px solid var(--tc-hair);
}
.tc-rail-toggle {
flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
width: 30px; height: 30px; padding: 0; border: 1px solid var(--tc-hair);
background: transparent; color: var(--tc-ink-idle); cursor: pointer;
}
.tc-rail-toggle:hover { background: var(--tc-hair); color: var(--tc-on-ink); }
.tc-rail-nav { flex: 1; display: flex; flex-direction: column; }
/* An item is a fixed-height icon row, so collapsed the rail is a column of even squares. */
.tc-rail-item {
position: relative; display: flex; align-items: center; gap: var(--space-3);
padding: 0 var(--space-4); min-height: 42px;
border: none; border-bottom: 1px solid var(--tc-hair); border-left: 4px solid transparent;
background: transparent; color: var(--tc-ink-idle);
font-family: var(--font-body); font-size: 14px; font-weight: 500; text-align: left;
text-decoration: none; cursor: pointer; width: 100%;
}
.tc-rail-item:hover { background: var(--tc-hair); color: var(--tc-on-ink); }
.tc-rail-item:disabled { opacity: 0.45; cursor: not-allowed; }
/* The accent marks the screen you are on as a rule down its edge, not as the label's colour: the
vermilion is 3.5:1 against the rail, which is enough for a 4px bar and not enough for a 14px
word. The word turns paper-white and heavy instead, so the item is marked twice over. */
.tc-rail-item.active { border-left-color: var(--color-accent); background: var(--tc-ink); color: var(--tc-on-ink); font-weight: 800; }
.tc-rail-icon { flex: 0 0 18px; display: inline-flex; align-items: center; justify-content: center; color: var(--tc-ink-muted); }
.tc-rail-item:hover .tc-rail-icon, .tc-rail-item.active .tc-rail-icon { color: var(--color-accent-300); }
.tc-rail-label { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.tc-rail-meta { padding: var(--space-3) var(--space-4); font-size: 11px; line-height: 1.7; color: var(--tc-ink-muted); letter-spacing: 0.08em; text-transform: uppercase; }
.tc-rail-foot { border-top: 1px solid var(--tc-hair); }
.tc-rail-foot .tc-rail-item:last-child { border-bottom: none; }
.tc-rail-role { display: inline-flex; margin-left: 6px; padding: 1px 6px; font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; background: var(--tc-hair); color: var(--tc-ink-idle); }
.tc-rail-live { padding: 0 var(--space-4) var(--space-3); font-size: 11px; font-weight: 600; color: var(--color-accent-300); }
.tc-rail-item:focus-visible, .tc-rail-toggle:focus-visible { outline: 2px solid var(--color-accent-300); outline-offset: -3px; }
/* Collapsed: an icon rail. The label stays in the DOM and stays in the accessibility tree —
opacity, not display or visibility — so a screen reader still reads "Delivery Rounds" off an
item a sighted coordinator has to hover to name. */
/* The facility name carries the hairline under the brand when the rail is open; collapsed it is
hidden, so the brand block takes the rule over or the mark runs straight into the first icon. */
.tc-rail-narrow .tc-rail-brand { justify-content: center; flex-wrap: wrap; gap: var(--space-2); padding: var(--space-4) 0 var(--space-3); border-bottom: 1px solid var(--tc-hair); }
.tc-rail-narrow .tc-rail-word, .tc-rail-narrow .tc-rail-facility, .tc-rail-narrow .tc-rail-meta, .tc-rail-narrow .tc-rail-role { display: none; }
.tc-rail-narrow .tc-rail-item { justify-content: center; padding: 0; min-height: 44px; border-left-width: 3px; }
.tc-rail-narrow .tc-rail-label {
position: absolute; left: 100%; top: 50%; transform: translateY(-50%); margin-left: 2px;
padding: 7px 10px; background: var(--tc-ink); color: var(--tc-on-ink);
border: 2px solid var(--tc-hair); font-weight: 600; font-size: 13px;
opacity: 0; pointer-events: none; z-index: 40; transition: opacity 90ms linear;
}
.tc-rail-narrow .tc-rail-item:hover .tc-rail-label,
.tc-rail-narrow .tc-rail-item:focus-visible .tc-rail-label { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .tc-rail-narrow .tc-rail-label { transition: none; } }
/* The page head carries the ink the full width of the content column, so it bleeds back out
* through main's gutter.
*
* Screens put whatever they like in this band — a counted total, a supplier filter, a "net $412"
* in accent-700 — and most of it names its colour inline as var(--color-text) or
* var(--color-neutral-700), which on ink is either invisible or close to it. Rather than make
* fourteen screens special-case the band, the band remaps those tokens for everything inside it:
* an inline var(--color-text) resolves to paper in here and to ink everywhere else. This is the
* only place in the product where a --color-* token takes a different value, it happens on a
* descendant of .tc-shell, and no phone screen has one of those. */
.tc-shell .page-head, .tc-shell .tc-pagehead {
--color-text: #f3f2f2;
--color-bg: #201e1d;
--color-surface: #37332f;
--color-divider: #37332f;
--color-neutral-200: #37332f;
--color-neutral-300: #57534f;
--color-neutral-600: #b5b1af;
--color-neutral-700: #b5b1af;
--color-neutral-800: #d6d3d2;
--color-accent-700: #ffc4b8;
background: var(--tc-ink); color: var(--tc-on-ink);
margin: 0 calc(var(--tc-gutter) * -1) var(--space-6);
padding: var(--space-6) var(--tc-gutter) var(--space-5);
border-bottom: 4px solid var(--color-accent);
display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap;
}
/* A field keeps its white ground, so inside one the ink has to come back or what someone types is
white on white — the reporting month select and the register's search box both live up here. */
.tc-shell .page-head .input, .tc-shell .tc-pagehead .input { --color-text: #201e1d; }
.tc-shell .page-head .h1, .tc-shell .tc-pagehead .h1 { color: var(--tc-on-ink); }
/* The figure a screen is actually about: a count, a value, a number of gaps. */
.tc-figure { font-family: var(--font-heading); font-weight: 800; font-size: 28px; line-height: 1.05; letter-spacing: -0.02em; }
.tc-meta { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); }
/* Tiles: the figures that sit under a page head. auto-fit rather than a fixed count, because the
same strip carries three tiles on Ordering and five on Reports. */
.tc-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); gap: 2px; background: var(--color-text); border: 2px solid var(--color-text); }
.tc-tile { background: var(--color-bg); padding: var(--space-4); display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.tc-tile-label { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--color-neutral-700); }
.tc-tile-note { font-size: 12px; color: var(--color-neutral-700); }
/* Panels: a bordered block with a named head. */
.tc-panel { border: 2px solid var(--color-text); background: var(--color-bg); min-width: 0; }
.tc-panel-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 2px solid var(--color-text); font-size: 12px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
.tc-panel-aside { font-size: 12px; font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--color-neutral-700); }
.tc-panel-body { padding: var(--space-4); }
.tc-panel-foot { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--color-divider); }
/* A list of rows fills its panel edge to edge — no body padding, the rows carry their own. */
.tc-panel-list > .tc-row:last-child { border-bottom: none; }
/* Rows: one garment, one order, one request. */
.tc-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--color-divider); min-width: 0; }
.tc-row-main { flex: 1; min-width: 0; }
.tc-row-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; }
.tc-row-meta { font-size: 12px; color: var(--color-neutral-700); }
.tc-row-fig { font-family: var(--font-heading); font-weight: 800; font-size: 18px; white-space: nowrap; }
a.tc-row, button.tc-row { width: 100%; background: var(--color-bg); border-left: none; border-right: none; border-top: none; color: inherit; text-decoration: none; text-align: left; font-size: 14px; cursor: pointer; }
a.tc-row:hover, button.tc-row:hover { background: var(--color-neutral-200); color: inherit; }
/* Anything that wants attention.
*
* The accent is already the brand — it is the primary button and it is the current nav item — so
* a second red a metre away across the room is a guess, not a signal. A flagged tile or row is
* marked three ways instead: a rule down its left edge, its figure heavier and in the darker red
* that stays legible on paper, and .tc-mark next to a word saying what is wrong. The mark is
* decoration and belongs behind aria-hidden; the word is the message, so screens keep the word. */
.tc-flag { border-left: 4px solid var(--color-accent); }
.tc-tile.tc-flag, .tc-row.tc-flag { padding-left: calc(var(--space-4) - 4px); }
.tc-flag .tc-figure, .tc-flag .tc-row-fig { font-weight: 800; color: var(--color-accent-700); }
.tc-flag .tc-tile-label { color: var(--color-accent-700); }
.tc-mark { display: inline-block; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-bottom: 9px solid var(--color-accent); vertical-align: -1px; margin-right: 6px; }
.tag-flag { background: var(--color-accent-600); border-color: var(--color-accent-600); color: #fff; gap: 5px; }
.tag-flag::before { content: ""; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; border-bottom: 7px solid currentColor; }
/* On paper the ink band is a trap: browsers drop background colours out of a print by default, and
what is left is paper-white type on white paper — a screen title that prints as nothing. So the
head goes back to the light treatment for the printer, and the gutter it bleeds through goes to
zero along with main's padding. */
@media print {
.tc-shell { --tc-gutter: 0px; }
.tc-shell .page-head, .tc-shell .tc-pagehead {
--color-text: #201e1d;
--color-bg: #f3f2f2;
--color-surface: #eae9e9;
--color-divider: #cfcccb;
--color-neutral-200: #e4e2e1;
--color-neutral-300: #d6d3d2;
--color-neutral-600: #6c6764;
--color-neutral-700: #57534f;
--color-neutral-800: #3a3735;
--color-accent-700: #b8240e;
background: none; color: var(--color-text);
margin: 0 0 var(--space-4); padding: 0 0 var(--space-2);
border-bottom: 2px solid var(--color-text);
}
.tc-shell .page-head .h1, .tc-shell .tc-pagehead .h1 { color: var(--color-text); }
}