ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
+501
@@ -0,0 +1,501 @@
|
||||
/* 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); }
|
||||
}
|
||||
Reference in New Issue
Block a user