/* 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,"); 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