put.io design system
Tokens, primitives and component specs for put.io. Generic
CSS / JSON here; platform apps adapt natively.
v2 · 2026-08
tokens.css ↗
Pipeline
Sourcetokens/*.tokens.json · canonical DTCG
Generatorscripts/build-tokens.ts
Outputsystem/tokens.css · generated
Basetokens/foundation.css · static layer
TargetsCSS now · Figma / iOS / Android via the same graph
Consume
CSS
DTCG JSON
Build
/* generated output — import in any page */ @import "system/tokens.css"; /* use vars as roles, never literals */ .btn-primary { background: var(--yellow-solid); color: var(--primary-foreground); border-radius: var(--radius); }
// tokens/*.tokens.json · W3C DTCG — paths map 1:1 to CSS vars { "yellow": { "solid": { "$value": "hsl(44.7, 97.9%, 63.1%)", "$type": "color", "$description": "Brand · #FDCE45 · → --yellow-solid" } }, "radius": { "radius": { "$value": "6px", "$type": "dimension", "$description": "Default radius · → --radius" } } }
// regenerate after editing tokens/*.tokens.json $ node scripts/build-tokens.ts // any tool that speaks DTCG can read the same graph read("tokens/*.tokens.json") // Style Dictionary, Tokens Studio, … // platform adapters live in app repos, not here
House rules
- Utility is beautiful. Beauty comes from making file operations feel premium — not from decoration.
- Content-agnostic by default. No posters, album art, curated metadata, or “watched” state. Filename, size, date.
- Raw filenames stay raw. Never parse, rewrite or fake metadata from a filename. Render verbatim, mono.
- Clarity over cleverness. One verb per icon, one icon per verb. Download / Save / Stream never overlap.
- Type carries the weight. Three families — sans for UI, two monos for numerics and identifiers.
- One yellow at a time.
#FDCE45is the single CTA and the focus ring. Not decoration. - TV is list-first. Without thumbnails, rows beat card walls. Don’t copy Apple TV or Plex.
- Voice and restraint. Plain, short, English-first. Subtract before you add.
Part A
Tokens & primitives
Foundations (color, type, space, radii, motion) followed by brand primitives (logo, icons, focus ring). Canonical source:
tokens/*.tokens.json — tokens.css and every other artifact is generated from it.A.01
Color
12-step scales, light + dark. One brand, three semantic, one transfer-state lime.
- Purpose
- Role-based color tokens. Vars name the role (
--bg,--text-secondary), not the hex. Components consume roles, never literals. - Source of truth
tokens/*.tokens.json(canonical) →tokens.css— light:root+.darkoverlay- Usage
/* light + dark in one stylesheet — toggle .dark on <html> */ .btn { background: var(--yellow-solid); color: var(--primary-foreground); } .row { background: var(--bg-secondary); color: var(--text); } .row:hover { background: var(--list-item-bg-hover); }
- Constraints
--yellow-solidis fill-only. For yellow text, use--yellow-text-secondary(AA on light bg).- No new color literals in component code
- No semantic scale for decoration — status only
Brandstep 9 shifts between modes; everything else flips like a normal 12-step scale
BRAND
--yellow-solid
#FDCE45
Brand fill: CTAs, focus halos (35% alpha), folder icons, progress bars. Never as text on light.
+ hover
--yellow-solid-hover
#F3C435
CTA hover — slightly darker brand yellow. Yellow can't darken much without going olive; this is the most we can shift.
contrast
--primary-foreground
#292109
Label text on yellow-solid — warm dark, not pure black.
yellow-contrast.surface
--yellow-bg
step 1 · #FEFDFA / #000
Faintest tint backdrop in light; drops to pure black at the deepest step in dark.
Neutrals · dark (app default)12 steps · gray scale
--bg
hsl(0 0% 8.5%)
Page background — the dominant app surface. Sidebars, body, full-screen modals.
--bg-secondary
hsl(0 0% 11%)
Inset panel · quiet aside. Code blocks, reference panels, secondary nav.
--component-bg
hsl(0 0% 13.6%)
Card surface · default button fill · chips. Focal components.
--component-bg-hover
hsl(0 0% 15.8%)
Hover state for any
--component-bg surface.--component-bg-active
hsl(0 0% 17.9%)
Pressed / selected state · toggle on.
--line
hsl(0 0% 20.5%)
Hairline divider between rows, cells, sections.
--border
hsl(0 0% 24.3%)
Component border — inputs, buttons, cards. Resting state.
--border-hover
hsl(0 0% 31.2%)
Border on hover / focus-within.
--solid
hsl(0 0% 43.9%)
Solid neutral fill —
btn-info, mid-tone icons.--solid-hover
hsl(0 0% 49.4%)
Hover companion to
--solid.--text-secondary
hsl(0 0% 62.8%)
Meta copy — captions, hints, timestamps, secondary nav.
--text
hsl(0 0% 93%)
Primary text — body, headings, button labels.
Semantic · positive · destructive · in-motionstatus only, never decoration
--green-solid
positive
btn-success · check icons · progress complete.--green-bg-secondary
positive surface
Success callout backdrop · confirmation tint.
--red-solid
destructive
btn-danger · error icons · invalid input border.--red-bg-secondary
destructive surface
Error callout backdrop · invalid input fill.
--lime-3
in-motion accent
Transfer downloading / completing rows. put.io-specific.
specimens
Color · canonical previewspreview/02 · 03 · 13
A.02
Type
Two local families. GT America for UI / body / display. Berkeley Mono for numerics, code & filenames.
- Purpose
- One sans + one mono do all the work. The mono role enforces raw filenames stay raw — filenames, numerics, and machine strings all render in Berkeley Mono.
- Source of truth
tokens/*.tokens.json(type group) →tokens.css· brand fonts from put.io's font CDN, system fallbacks- Generated
- CSS custom properties (
--font-*,--fs-*) - Usage
/* font roles */ .row { font-family: var(--font-sans); } /* GT America */ .row .size { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } /* Berkeley · numerics */ .row .name { font-family: var(--font-mono); } /* Berkeley · filenames */
- Constraints
- Weights used: 400 · 500 · 700 · 900 (display only)
- Mobile body never below 14px · TV body never below 24px
- No display serif, no script, no system stack fallbacks for body
--fs-display
put.io
--fs-3xl
Display heading
--fs-2xl
Display heading · small
--fs-xl
Page title
--fs-lg
Heading
--fs-md
Subheading · card title
--fs-base
Body — app default (= root, matches production fontSize 1)
--fs-sm
Caption · button label (production fontSize 0)
--fs-xs
Fine print / overline
--font-mono
11.20 TB · 12.4 MB/s · 00:03:42
--font-mono
ubuntu-22.04.iso · sha1: e3b0c44…
specimens
Type · canonical previewspreview/04 · 05 · 06
A.03
Spacing
4 px base, aggressive doubling. Reserved for layout — components handle inner padding via their own tokens.
- Purpose
- Layout rhythm.
--space-3(16) is the row gutter;--space-4(32) is the section gutter. - Generated
spacing.json· platform px / dp scales- Usage
.list { display: flex; flex-direction: column; gap: var(--space-1); } /* 4px between rows */ .section { padding: var(--space-4) var(--space-3); } /* 32 × 16 */
- Constraints
- Use scale tokens — no arbitrary px values in components
- No half-steps (no 6px / 12px / 24px)
A.04
Radii & shadow
Five radii, four elevations. 6 px is the default for buttons, inputs, cards. Yellow halo is one of the shadows.
- Purpose
- Radii match component scale. Shadows define elevation roles — hover/focus, popover, modal — and a yellow focus halo that is the focus system.
- Source of truth
tokens/*.tokens.json→tokens.css·--radius-*·--shadow-*- Generated
- CSS custom properties
- Usage
.btn { border-radius: var(--radius); } /* 6px default */ .pill { border-radius: var(--radius-pill); } .btn:focus-visible { box-shadow: var(--shadow-focus); } /* yellow halo */
- Constraints
--radius= 6 is the canonical default- Only
--shadow-focusever uses yellow - No drop shadow on hover — use bg shift instead
--radius-sm
4px
--radius
6px · default
--radius-md
8px
--radius-lg
10px
--radius-pill
999px
--shadow-sm
hover / focus
0 1px 2px / 0.4
0 1px 2px / 0.4
--shadow-md
popover, menu
0 4px 12px / 0.5
0 4px 12px / 0.5
--shadow-lg
modal, sheet
0 16px 40px / 0.6
0 16px 40px / 0.6
--shadow-focus
soft halo
0 0 0 3px / 0.35
0 0 0 3px / 0.35
A.05
Motion
Two easings. Three durations. If a transition doesn't fit one of these, it's wrong.
- Purpose
- Animation feels consistent across platforms.
--dur-fastfor hover,--dur-basefor menus,--dur-slowfor sheets. - Source of truth
tokens/*.tokens.json→tokens.css·--ease-*·--dur-*- Generated
- CSS custom properties · native easing curves
- Usage
.btn { transition: background var(--dur-fast) var(--ease-out); } .sheet { transition: transform var(--dur-slow) var(--ease-in-out); }
- Constraints
- Respect
prefers-reduced-motionat the component level - No bounce / overshoot easings
- Respect
A · 2
Brand & primitivespreview/00 · 01 · 09 · 11 · 12
A.06
Brand & logo
Logomark, app-icon family, empty-state catalogue. Kaomoji
ᕦ(ò_óˇ)ᕤ is part of the brand.- Purpose
- Brand identity assets. Logomark = wordmark + dot; app icon = mark on yellow square; empty-state = kaomoji + one-line copy + one yellow CTA.
- Source of truth
system/assets/logo.svg·logo-retro-{dark,light}.svg·app-icon-*.png- Generated
- iOS / Android / web favicons · app icons per platform
- Usage
<!-- always lowercase --> <span class="brand">put<span class="dot">.</span>io</span>
- Constraints
- Wordmark is always lowercase put.io — never upper-cased, title-cased, or dot-less
- No mark in body text · no shrunk-then-cropped variants
A.07
Icons
Phosphor going forward.
ph-fill ph-folder tinted #FDCE45 is the signature.- Purpose
- One icon family across all platforms. Regular weight default · fill weight for active states. Inline SVG only — no icon fonts in new code.
- Source of truth
- Phosphor icons ·
phosphor-icons.com - Generated
- Inline-SVG icon set · per-icon metadata JSON
- Usage
<!-- inline SVG, 24×24, currentColor --> <svg width="24" height="24" viewBox="0 0 256 256"> <path fill="currentColor" d="…"/> </svg>
- Constraints
- 24×24 default, 20×20 in dense tables, 32×32 on TV
- File-browser icons are yellow for every kind —
--file-row-iconon folder, video, audio, image, archive, generic file. TV is yellow throughout - Utility / chrome icons stay neutral — sidebar nav, toolbars, settings, history, menus
- No emoji as icons · no decorative SVG
- No legacy Flaticons font in new code
A.08
Focus
Two focus languages. Keyboard and pointer resolve to the brand-yellow halo; D-pad resolves to a surface fill, because a 10-foot screen reads fills better than rings.
- Purpose
- Web/mobile = soft 3px brand-yellow halo (35% alpha). TV = a solid surface fill: rows go transparent →
--component-bg-active, buttons also step their border to--border-hover. No ring, scale, shadow, or halo on TV. - Source of truth
tokens/*.tokens.json→tokens.css·--shadow-focus- Generated
- TV variant via
tv.css - Usage
/* web/mobile */ .btn:focus-visible { box-shadow: var(--shadow-focus); } /* TV — a fill, never a lift */ .tv .row.focused { background: var(--component-bg-active); } .tv .btn.focused { background: var(--component-bg-active); border-color: var(--border-hover); }
- Constraints
- Yellow is mandatory for web and mobile focus — it is the one place the brand color is required. TV is the exception: focus there is a surface fill, never yellow
- Use
:focus-visibleon web/mobile, never plain:focus - No outline removal without a visible replacement
Part B
Components
Reusable, composable, framework-agnostic specs. Each card answers purpose · source · artifact · usage · constraints, then shows the canonical specimen.
source · preview/components-*
preview/ ↗
B.02
Inputs
Text, search (with / hint), password, error. Yellow border + halo on
:focus-visible.- Purpose
- Text entry primitive. Search box, password field, validation states use the same shell.
- Source of truth
Input- Generated
- CSS class set · platform-native field shells
- Usage
<input class="input" type="text" placeholder="Search files"> <input class="input is-error" aria-invalid="true">
- Constraints
- Label outside the field · placeholder is hint, not label
- Error message uses
--red-solidbelow the field - No placeholders as labels
B.03
Form fields · extended
Date · stepper · slider · tag chips · autocomplete · file row. Plus the full validation vocabulary — error / warning / success / async / counted.
- Purpose
- Field types beyond plain text. Each inherits Input's shell + adds a behavior. Validation vocabulary is shared.
- Source of truth
Field/*- Generated
- CSS variants · native pickers per platform
- Usage
<FieldDate name="due" value={d} onChange={set} /> <FieldSlider min={0} max={100} step={5} />
- Constraints
- Validation state is a prop, not a class — one source per field
- No custom-built date picker — defer to platform native on mobile
B.04
Form layouts
Seven canonical compositions. Settings row (label-left) · modal stacked · two-column · inline composer · wizard · sticky save bar · click-to-edit.
- Purpose
- Composition patterns. Pick the closest match — don't invent a new one.
- Source of truth
FormLayout- Generated
- Grid templates · responsive breakpoints
- Usage
<FormLayout variant="settings-row"> ... </FormLayout> <FormLayout variant="two-column"> ... </FormLayout>
- Constraints
- Sticky save bar appears only on dirty state
- No mid-form modals — break into a wizard instead
B.05
Badges & chips
Lifecycle pills, filter chips, user tags. No codec / quality / "watched" badges — see content-agnostic.
- Purpose
- Status communication. Lifecycle (queued/in-progress/done/failed), filter chips, user tags.
- Source of truth
Badge·Chip- Generated
- CSS classes · semantic color mapping
- Usage
<Badge tone="positive">Completed</Badge> <Badge tone="in-motion">Downloading</Badge> <Chip selected>Video</Chip>
- Constraints
- Tone maps to semantic scale (positive/destructive/in-motion)
- No codec / quality / poster badges on files
- No "watched" state — we don't track that
B.06
List items
The most-touched primitive. Sidebar nav · setting row · friend row · activity row — same skeleton, four shapes.
- Purpose
- Single horizontal row primitive. Composes leading icon · primary text · secondary text · trailing meta · trailing action.
- Source of truth
ListItem- Generated
- Slot-based CSS · TS prop types
- Usage
<ListItem leading={icon} title="Files" trailing={count} /> <ListItem title="Theme" subtitle="Auto" layout="setting" />
- Constraints
- Min row height 44px (touch) · 48px (TV focus margin)
- Trailing meta uses
--font-mono - No more than one trailing element
B.07
File row
The content-agnostic file primitive. Yellow icon for every file kind, mono filename, mono size, mono date. Same skeleton in web tables, mobile lists, TV rows.
- Purpose
- Render any file/folder identically: icon · filename · size · date. No metadata enrichment, ever.
- Source of truth
FileRow- Generated
- Web table / mobile cell / TV row variants
- Usage
<FileRow kind="folder" // folder | video | audio | doc | archive | other name="The.Wire.S03E04.1080p.x264-GROUP.mkv" // raw, verbatim size="1.42 GB" date="2026-05-25" />
- Constraints
- Filename always Berkeley Mono, ellipsis-on-overflow, hover reveals full
- Every
kindtakes--file-row-icon— the folder is not a special case - No parsing, no marketing-group stripping
- No thumbnail · no codec badge · no resolution chip
B.08
Storage bar
Healthy (with file-type breakdown) · warn 80% · over 95%. Compact sidebar variant + plan-upsell card.
- Purpose
- Quota readout. Three thresholds (healthy / warn / over) drive color. Breakdown is by file-type, not metadata.
- Source of truth
StorageBar- Generated
- Web sidebar + standalone card
- Usage
<StorageBar used={11_200_000_000_000} total={50_000_000_000_000} breakdown={["video", "audio", "doc"]} />
- Constraints
- Threshold colors: green ≤80, yellow 80-95, red >95
- No "junk file" or "biggest movie" labels — we don't know that
B.09
Transfer states
Queued → downloading → completed / seeding / failed. Health indicator replaces torrent jargon (seed ratio, peer count).
- Purpose
- The "is it working?" answer. One health dot (green/yellow/red) plus a one-line status. Power-user detail (ratio, peers) is in expand.
- Source of truth
TransferRow- Generated
- Web + mobile + TV transfer-row variants
- Usage
<TransferRow state="downloading" // queued | downloading | seeding | completed | failed health="good" // good | warn | bad progress={0.62} name="ubuntu-22.04.iso" />
- Constraints
- Default view is plain-English: "Downloading · 62%"
- Lime accent (
--lime-3) for in-motion surface only - No "Seed ratio 2.00/10 days" in default view
B.10
Empty & error states
Kaomoji + one-line copy + one yellow CTA. Loading skeleton above 200ms wait. Red error sheet for real failures.
- Purpose
- Communicate "nothing here yet" or "something broke" with personality and a single clear next action.
- Source of truth
EmptyState·ErrorSheet- Generated
- Per-platform empty illustrations as text/kaomoji
- Usage
<EmptyState kao="ᕦ(ò_óˇ)ᕤ" title="No transfers yet" cta={{ label: "Add a transfer", onClick: ... }} />
- Constraints
- One CTA · one kaomoji · one line of copy
- Loading skeleton kicks in at 200ms, not earlier
- No illustration art · no decorative SVG
B.11
Overlays
Dialog · detail sheet · bottom-sheet · confirm. One scrim, one elevation. Bottom-sheet is mobile-first; the others render anywhere.
- Purpose
- Modal surfaces. Dialog (centered) · detail sheet (right edge) · bottom-sheet (mobile) · confirm (small destructive).
- Source of truth
Overlay/*- Generated
- CSS + ARIA primitives · native sheets on iOS/Android
- Usage
<Dialog open={o} onClose={c}> ... </Dialog> <BottomSheet open={o} onClose={c} actions={[ ... ]} />
- Constraints
- Scrim opacity from
--overlay-fullonly - Bottom-sheet primary action at top; destructive in red
- No nested overlays
- Scrim opacity from
B.12
Other primitives
Avatars · breadcrumbs · command palette · menus · notification · tabs · tooltip. Same token rules, same focus model.
- Purpose
- Smaller, less-touched primitives. Lifted verbatim from the same token roles as everything else.
- Source of truth
Avatar / Breadcrumbs / CommandPalette / Menu / Notification / Tabs / Tooltip- Generated
- Per-platform variants
- Usage
<Avatar name="kai" /> <Breadcrumbs path={["Files", "Movies"]} /> <Tabs items={t} /> <Tooltip content="…">…</Tooltip>
- Constraints
- Tooltips: keyboard-accessible, dismissible on Esc
- Notifications: max 3 stacked · auto-dismiss 4s for info, manual for destructive
Part C
Platforms
Where tokens and components compose into the surfaces users actually touch, ordered by binding tier. This repo publishes generic CSS / JSON; platform repos adapt locally. Visual stays put.io.
C.00
Platform contract · binding tiers
A web-rendered mock is never a native spec. Every specimen card declares its tier in a strip above the artboard (
preview/_tier.css); the tier decides how much of the system binds.- Purpose
- Clarify the seam between generic spec (here) and platform implementation (in each app repo). Spec changes propagate via artifact regeneration, not copy-paste.
- Source of truth
tokens/*.tokens.json+preview/*.htmlspecimens +platforms/*/DESIGN.mdcontracts- Generated
- CSS · DTCG JSON · flat JSON · TypeScript metadata · Figma export
- Usage · the five tiers
0 Foundations // the token graph. Binds every tier — values only 1 www · app · auth // the FULL system: components.css recipes are the contract 2 iOS · watchOS · tvOS · // tokens only. Every control comes from the Android · Android TV // platform HIG / Material 3 3 Roku // tier-2 tokens + put.io conventions in custom SceneGraph 4 tv.put.io · Tizen · webOS // the web system, restrained to a list-first 10-foot UI
- Constraints
- Tier-1/3/4 cards are Components — put.io owns the recipe. Tier-2 cards are Elements — the platform owns the control; the card documents how it receives tokens
- Never read an Element card as a build-a-custom-control instruction
- Every file-type icon is brand yellow, Phosphor, on every tier
- No platform-specific color in this repo
- No component recipes on tier 2 — tokens only
// distribution model this design system // tokens, component specs, flow specs // ships: CSS · DTCG JSON · TS · Figma export // consumes the generated CSS bundle the web apps // desktop + smart-TV · same CSS the native apps // iOS / Android / Roku — build their OWN // adapters from the DTCG JSON; this system // doesn't ship Swift / Kotlin / BrightScript
C · 1
Web · tier 1 · www / app / auththe recipes are the contract here, and only here
C.01
App shell
Collapsible sidebar · breadcrumb top bar with /-search · selection toolbar · column-headed file table.
- Purpose
- Top-level web layout. Sidebar nav + main content + storage footer. Keyboard-first.
- Source of truth
- the web app · main layout
- Generated
- React component · CSS grid template
- Usage
<AppShell sidebar={<Sidebar />} topbar={<TopBar />}> <FilesView /> </AppShell>
- Constraints
- Sidebar collapsible; persisted preference
- One yellow CTA on the topbar (New transfer)
- No second yellow button anywhere in the shell
C.02
Web screens
Rebuilds of shipped
app.put.io routes — match them. Per-control detail lives on the component cards in Part B; these carry composition, flow and content rules.preview/web-s*
C · 2
Apple · tier 2 · iOS / iPadOS / watchOS / tvOStokens only · every control from the HIG
C.03
iOS elements
One stock control per card: its states, a spec strip, a don’t block. The card documents how the control receives put.io tokens (
.tint(), Dynamic Type) — never how to rebuild it. Read the Element card before the Screen card. Contract: platforms/apple/DESIGN.md.preview/ios-e*
C.04
Apple screens
iOS 26 Liquid Glass, correctly scoped: glass lives in the floating layer only — tab bar, toolbar, sheet chrome, floating buttons. Lists, rows, forms and full-screen content stay opaque
--app-bg. Never glass over glass. The one composed control is the download Gauge(.accessoryCircularCapacity).preview/ios-s* · watchos · tvos
C · 3
Android · tier 2 · Android / Android TVMaterial 3, not a port of iOS
C.05
Android elements
Stock Material 3 components receiving the graph. The Android and iOS switches are both yellow and deliberately do not match — that divergence is the tier working. Contract: platforms/android/DESIGN.md.
preview/android-e*
- Purpose
- The entire tier-2 integration on Android is one
darkColorScheme(…)filled from the graph. - Usage · colour-scheme mapping
primary → --yellow-solid onPrimary → --primary-foreground surface → --app-bg onSurface → --text onSurfaceVariant → --text-secondary surfaceContainer → --component-bg surfaceContainerHigh → --component-bg-hover surfaceContainerHighest → --component-bg-active outline → --border outlineVariant → --line error → --red-solid
- Constraints
- No iOS conventions: no section footers, no steppers, no inset-grouped lists, no action sheets
- No hand-written colour schemes in app repos — generate from the graph
C.06
Android screens
M3 top app bar, nav bar, FAB and lists; Compose for TV on the 10-foot shell. Android TV scales and elevates on focus without tilt — the platform’s own engine, not tvOS’s.
preview/android-s* · androidtv
C · 4
Roku · tier 3tokens + put.io conventions in custom SceneGraph components
C.07
Roku channel
Authored 1:1 at FHD 1920×1080 on a 3px grid — 102px page margin, 1716×120 rows, 3px borders, nine-patch focus. Stock Overhang, Dialog, Keyboard and Video nodes; the list row, settings row and transfer row are custom SceneGraph components built from graph values. Focus is a footprint — fill plus a 2px
--border-hover edge, no z-axis. Contract: platforms/roku/DESIGN.md.preview/roku-*
Icon names are not Phosphor names on Roku. Card asset names map through
config/phosphor-icons.json in the Roku channel repo — never assume the card’s glyph name is the asset name.
C · 5
TV (generic) · tier 4 · tv.put.io / Tizen / webOSthe web system, restrained to a list-first 10-foot interface
C.08
Foundations
Row anatomy · type scale at 10ft · surface materials · content rules. List-first — see house rules.
- Purpose
- 10-foot UI primitives. Filename-driven; no thumbnails, no posters. Authored 1:1 at 1920×1080 — a px in the file is a px on the TV.
- Source of truth
- the
tvtoken group in the DTCG graph (type steps, spacing, overscan, radius, z) ·system/tv.css· the TV specimen pages below - Generated
- CSS scope (
.tv) · DTCG TV-context tokens - Usage
/* the tv scale: heading 64 · label 48 · body 36 · caption 32 */ .tv .row .title { font-size: 36px; } /* GT America, raw filename */ .tv .row .sub { font-size: 32px; font-variant-numeric: tabular-nums; } /* no mono on any TV tier — TV numerics are GT America tabular figures */
- Constraints
- List rows, not card grids · D-pad navigation only
- Overscan safe area from
tv.overscan.x/yratios (77px / 22px at 1920×1080) - No mono face on TV · no posters · no hero rails · no “Continue Watching” if we can’t track it
C.09
Focus
A fill, never a lift —
--component-bg-active, plus --border-hover on anything already bordered. No scale, shadow, halo, or white-invert, and applied instantly. Each TV tier keeps its platform’s own engine: tvOS lifts, Android TV elevates, Roku draws a footprint, tv.put.io fills — all on the same row anatomy.C.12
Player chrome
Scrubber, audio/subtitle pickers, info overlay. Player engines and controls are platform-specific (AVPlayer / Compose for TV / Roku / HTML5); the shared token graph keeps the family aligned without crossing binding tiers.
C.13
TV screens
Search, history, trash, system states, and device-code auth. History carries finished outcomes; there is no transfers screen or browse-time continue-watching rail.
preview/tv-s03…s07
C.14
Platform matrix
Seven platforms × five differences (focus engine, surface material, player ownership, perf floor, token mapping).
Pending preview
Strategy & token mapping table
Web · iOS · Android · Apple TV · Android TV · Roku · Smart-TV web. Each consuming platform repo owns its own engine, surface, player-ownership and token-mapping breakdown.
C.15
Specimen file map
Every TV preview embedded above is a flat HTML file in
system/preview/. Native app repos consume the DTCG JSON for tokens and lift these specimens verbatim into their own idiom.system/preview/tv-*
The specimen is the contract. When implementing a TV component natively, open the matching
preview/tv-*.html + system/tv.css, read the exact values (font-size, weight, tracking, line-height, padding, radius, color tokens, shadow), then re-express them in your platform’s native language. No native bundles ship from this repo.
TV
Foundations
spec
TV
Focus
spec
TV
Navigation
spec
TV
Action menus
spec
TV
Select modal
spec
TV
Resume prompt
spec
TV
Conversion status
spec
TV
Account
spec
TV
TV player
spec
TV
Video player · controls
spec
TV
Search
spec
TV
History
spec
TV
Trash
spec
TV
System states
spec
TV
Device-code auth
spec
TV
TV stylesheet
css
Native consumers · out-of-treenot shipped from this repo
iOS
Apple TV · tvOS app
native
AT
Android TV / Google TV
native
RK
Roku channel
native
SM
Smart-TV web · Tizen / webOS
native