put.io design system

Tokens, primitives and component specs for put.io. Generic CSS / JSON here; platform apps adapt natively.
v0.6 · 2026-05 tokens.css ↗

Pipeline

Sourcetokens/**/*.tokens.json · canonical
Generatedsystem/tokens.css · build output
CSS@putdotio/design/css
DTCG@putdotio/design/tokens/dtcg
Flat JSON@putdotio/design/tokens
TS@putdotio/design/tokens/meta
Figma@putdotio/design/tokens/figma

Consume

CSS
DTCG JSON
TS
/* import the single source */
@import "@putdotio/design/css";

/* use vars as roles, never literals */
.btn-primary {
  background: var(--yellow-solid);
  color: #000;
  border-radius: var(--radius);
}
// @putdotio/design/tokens/dtcg · W3C DTCG
{
  "color": {
    "brand": {
      "yellow": { "$value": "hsl(44.7, 97.9%, 63.1%)", "$type": "color" }
    }
  },
  "radius": {
    "base": { "$value": "6px", "$type": "dimension" }
  }
}
// @putdotio/design/tokens/meta · typed metadata
import { tokens } from "@putdotio/design/tokens/meta";

tokens["color.brand.yellow"] // "hsl(44.7, 97.9%, 63.1%)"
tokens.radius.base          // "6px"

// platform adapters live in app repos, not here

House rules

  1. Utility is beautiful. Beauty comes from making file operations feel premium — not from decoration.
  2. Content-agnostic by default. No posters, album art, curated metadata, or “watched” state. Filename, size, date.
  3. Raw filenames stay raw. Never parse, rewrite or fake metadata from a filename. Render verbatim, mono.
  4. Clarity over cleverness. One verb per icon, one icon per verb. Download / Save / Stream never overlap.
  5. Type carries the weight. Three families — sans for UI, two monos for numerics and identifiers.
  6. One yellow at a time. #FDCE45 is the single CTA, button/nav/TV focus halo, and brand accent. Not decoration.
  7. TV is list-first. Without thumbnails, rows beat card walls. Don’t copy Apple TV or Plex.
  8. 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). Single source: tokens.css — every other artifact is generated from it.
A.01

Color

Radix 12-step scales, dark + light. One brand, three semantic scales, 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 — generated into system/tokens.css
Usage
/* light + dark in one stylesheet — toggle .dark on <html> */
.btn { background: var(--yellow-solid); color: #000; }
.row { background: var(--bg-secondary); color: var(--text); }
.row:hover { background: var(--list-item-bg-hover); }
Constraints
  • --yellow-solid = backgrounds, icons, text-on-dark only — 1.5:1 on light = fail
  • For yellow text on light, use --yellow-text-secondary (4.98:1 AA)
  • No new color literals in component code
  • No semantic scale for decoration — status only
Brand · sacrednever changes across modes or platforms
SACRED
--yellow-solid
hsl(44.7, 97.9%, 63.1%)
Brand fill: CTAs, button/nav/TV focus halos (35% alpha), folder icons, progress bars. Never as text on light.
hover · light
--yellow-solid-hover
hsl(44.7, 97.9%, 63.1%)
CTA hover keeps the sacred yellow value in light mode.
hover · dark
.dark --yellow-solid-hover
hsl(44.7, 97.9%, 63.1%)
CTA hover keeps the sacred yellow value in dark mode.
surface
--yellow-bg
hsl(45 100% 5.5%)
Faint brand tint backdrop (dark mode); near-white in light.
Neutrals · dark (app default)12 steps · radix gray-dark
--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 fields keep --field-bg.
--lime-3
in-motion accent
Transfer downloading / completing rows. put.io-specific.
--yellow-solid
brand · status accent
Primary CTA fill · button/nav/TV focus halo · active step · brand mark.
specimens

Color · canonical previewspreview/color-*

Brand yellowopen ↗
Gray scaleopen ↗
Semantic scalesopen ↗
Dark-mode surfacesopen ↗
A.02

Type

Three local families. GT America for UI / body / display. GT America Mono for numerics. Berkeley Mono for code & filenames.
Purpose
One sans + two monos do all the work. Mono families enforce raw filenames stay raw — filenames render in Berkeley Mono, numerics in GT Mono.
Source of truth
system/tokens.css · font CSS hosted at static.put.io/fonts/
Generated
type.tokens.json · CSS custom properties · TS metadata
Usage
/* font roles */
.row          { font-family: var(--font-sans); }      /* GT America */
.row .size    { font-family: var(--font-ui-mono); font-variant-numeric: tabular-nums; }
.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
96 · 900
put.io
--fs-3xl
48 · 700
Display heading
--fs-2xl
32 · 700
Section heading
--fs-xl
24 · 500
Subheading · card title
--fs-lg
18 · 400
Lead copy for long-form
--fs-md
16 · 500
Subheading · dense UI
--fs-base
14 · 400
Body — app default
--fs-sm
13 · 400
Secondary body
--fs-xs
12 · 500 · UP
Label / caption
--font-ui-mono
GT Mono · tabular
11.20 TB · 12.4 MB/s · 00:03:42
--font-mono
Berkeley · filenames
ubuntu-22.04.iso · sha1: e3b0c44…
specimens

Type · canonical previewspreview/type-*

Familiesopen ↗
Type scaleopen ↗
Specimenopen ↗
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)
Spacing in contextopen ↗
A.04

Radii & shadow

Five radii, four elevations. 6 px is the default for buttons and fields. The yellow halo is reserved for button, link, nav, and TV focus.
Purpose
Radii match component scale. Shadows define elevation roles — popover, modal, raised panels — plus a yellow focus halo for non-field controls.
Source of truth
system/tokens.css · --radius-* · --shadow-*
Generated
radii.json · shadows.json
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
  • --shadow-focus uses yellow; form fields use --field-ring
  • 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
--shadow-md
popover, menu
0 4px 12px / 0.5
--shadow-lg
modal, sheet
0 16px 40px / 0.6
--shadow-focus
yellow halo · 3px
0 0 0 3px / 0.35
Radii & shadows · canonical previewopen ↗
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-fast for hover, --dur-base for menus, --dur-slow for sheets.
Source of truth
tokens.css · --ease-* · --dur-*
Generated
motion.json · 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-motion at the component level
  • No bounce / overshoot easings
Easing & duration scaleopen ↗
A · 2

Brand & primitivespreview/brand-* · focus-system · icon-set

A.07

Icons

Phosphor going forward. regular line weight. ph-folder-fill 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
  • Folder icon = ph-folder-fill tinted --yellow-solid
  • No emoji as icons · no decorative SVG
  • No legacy Flaticons font in new code
A.08

Focus

Three input contexts, one focus contract. Buttons and nav get the brand halo; text entry gets the quiet field ring; TV focus scales for distance.
Purpose
Unified focus visual across runtimes. Buttons, links, and nav use the brand-yellow halo. Text fields and OTP slots use the quieter --field-ring. TV adds scale + drop shadow.
Source of truth
tokens.css · --shadow-focus · --field-ring
Generated
TV variant via tv-shell.css
Usage
/* web/mobile */
.btn:focus-visible { box-shadow: var(--shadow-focus); }
.field:focus-visible { box-shadow: var(--field-ring); }
/* TV — overrides for 10ft viewing */
.tv .card:focus { transform: scale(1.06); box-shadow: 0 0 0 4px var(--yellow-solid), var(--shadow-lg); }
Constraints
  • Use yellow focus for buttons, links, nav, and TV focus
  • Use --field-ring, not yellow, for text entry controls
  • Use :focus-visible on web/mobile, never plain :focus
  • No outline removal without a visible replacement
Focus contextsopen ↗
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.01

Buttons

5 variants × 3 sizes. Primary is brand-yellow on a black ink. Only one primary per region — see one yellow at a time.
Purpose
Primary CTA + secondary + ghost + destructive + icon. All states (hover, active, focus-visible, disabled) lift from token roles.
Source of truth
@putdotio/ui · Button.tsx
Generated
CSS class set · DTCG component tokens · TS prop types
Usage
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-ghost">More</button>
<button class="btn btn-destructive">Delete</button>
Constraints
  • One btn-primary per surface region
  • Min-width 96px web · 44px hit-target mobile · 88px focus-target TV
  • No icon-only button without aria-label
B.02

Inputs

Text, search (with / hint), password, error. Fields use --field-bg and a quiet --field-ring on focus.
Purpose
Text entry primitive. Search box, password field, validation states use the same shell.
Source of truth
@putdotio/ui · Input.tsx
Generated
CSS class set · platform-native field shells
Usage
<input class="field" type="text" placeholder="Search files">
<input class="field" aria-invalid="true" aria-describedby="field-error">
Constraints
  • Label outside the field · placeholder is hint, not label
  • Invalid state is aria-invalid="true": red border + red text, no red fill
  • No placeholders as labels
Inputsopen ↗
B.03

Form fields · extended

Date · stepper · slider · tag chips · autocomplete · file row · password strength. 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
@putdotio/ui · 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
@putdotio/ui · 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
@putdotio/ui · Badge.tsx · Chip.tsx
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
Pills · chips · tagsopen ↗
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
@putdotio/ui · ListItem.tsx
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-ui-mono
  • No more than one trailing element
B.07

File row

The content-agnostic file primitive. Yellow folder icon, 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
@putdotio/ui · FileRow.tsx
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
  • Folder = ph-folder-fill in --yellow-solid
  • 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
@putdotio/ui · StorageBar.tsx
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
Disk usageopen ↗
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
@putdotio/ui · TransferRow.tsx
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
@putdotio/ui · EmptyState.tsx · ErrorSheet.tsx
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 · sheet · bottom-sheet · confirm. One scrim, one elevation. Bottom-sheet is mobile-first; the others render anywhere.
Purpose
Modal surfaces. Dialog (centered) · sheet (right edge) · bottom-sheet (mobile) · confirm (small destructive).
Source of truth
@putdotio/ui · 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-full only
  • Bottom-sheet primary action at top; destructive in red
  • No nested overlays
B.12

Other primitives

Avatars · breadcrumbs · dropzone · 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
@putdotio/ui · Avatar / Breadcrumbs / Dropzone / 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
Avatarsopen ↗
Breadcrumbsopen ↗
Dropzoneopen ↗
Notificationsopen ↗
Tooltipsopen ↗
Part C

Platforms

Where tokens and components compose into the surfaces users actually touch. This repo publishes generic CSS / JSON; platform repos adapt locally. Visual stays put.io.
C.00

Platform contract

What lives where. This repo = framework-agnostic source of truth. Each platform repo consumes the generated artifact and implements natively.
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
system/tokens.css + preview/*.html specimens
Generated
CSS · DTCG JSON · flat JSON · TypeScript metadata · Figma export
Usage · per-platform consumption
// any web / JS runtime — install the CSS package
import "@putdotio/design/css";

// any tool that speaks DTCG JSON (Style Dictionary, Tokens Studio …)
read("@putdotio/design/tokens/dtcg") // → consume in your own build

// native app repos build their own adapter from the same JSON
Constraints
  • Platform repos may add — never override — token values
  • Visual matches across platforms; engine (focus, blur, player) is local
  • No platform-specific color in this repo
  • No "TV-only" tokens here — TV adaptation lives in putio-ios / android
// repo split
putio-design/        // THIS REPO — tokens, component specs, flow specs
                     // ships: CSS · DTCG JSON · TS · Figma export

product implementation// consumes @putdotio/design/css
putio-tv-web/        // lightweight JS runtime · same CSS
putio-{ios,android,roku}/  // build their OWN adapters from DTCG JSON
                     // — this repo doesn't ship Swift / Kotlin / BrightScript
B · 1

Web · desktop browserputdotio/product implementation

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
product app shell
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 references

Convergent ground-truth screens to lift from. Live in putdotio/product implementation; cross-project canvases mirror them here.
v12 – v15 · linear-neutral
Pending source Files list · Light + Dark putdotio/product implementation · product file browser surface
Pending source File detail / player Right-rail actions, breadcrumbs, Chromecast button. product file-detail surface
Pending source Command palette · ⌘K New surface. Compose from Inputs + List items.
Pending source Billing & plans Persona-shaped pricing for trial / casual / plus / power tiers. Composes from Buttons + List items.
B · 2

Mobile · iOS & Androidputio-ios · putio-android

C.03

iOS & Android shells

iOS Files screen · Android player chrome · shared pairing/activation flow used across iOS, Android, tvOS, Roku.
Purpose
Touch-first shells. iOS uses native nav stack; Android uses Material You scaffold tinted with brand yellow.
Source of truth
Platform repos (out-of-tree). Shells composed from @putdotio/ui primitives in each platform's idiom.
Generated
Spec only — platform repos implement natively
Usage · iOS
NavigationStack {
  FilesView()
    .toolbar { ToolbarItem(placement: .primaryAction) { AddTransferButton() } }
}
Constraints
  • Long-press → bottom-sheet for file actions
  • Destructive items use --red-solid text
  • No swipe-to-delete without confirm
C.04

Bottom-sheet actions

Mobile uses the bottom-sheet from Overlays for file actions. Long-press → bottom-sheet → tap; destructive in red.
C.05

Mobile references

Pending canonical sources — the new putio-ios & putio-android apps are being built spec-first.
Pending source iOS · Transfers queue Swipe-to-act rows. Compose from Transfer states + iOS shell.
Pending source iOS · Player + PiP AVPlayer-backed. Lock Screen / Dynamic Island controls.
Pending source Android · Files list Material You scaffold, brand-yellow accent override.
Pending source Settings · iOS / Android Compose from List items — Account · Plan · Privacy · Notifications · Apps & integrations · Storage groups.
C · 3

TV · Apple TV · Android TV · Roku · Smart-TVplatform repos adapt locally

C.06

Foundations

Row anatomy · type scale at 10ft · surface materials · content rules. List-first — see house rules.
tv-shell.css
Purpose
10-foot UI primitives. Filename-driven; no thumbnails, no posters. Rows of 56-72px focus targets.
Source of truth
system/preview/tv-shell.css + the TV specimen pages below
Generated
CSS scope (.tv) · DTCG TV-context tokens
Usage
/* TV body never below 24px */
.tv .row      { font-size: 28px; line-height: 1.25; padding: 18px 32px; }
.tv .row .name { font-family: var(--font-mono); }    /* raw filename */
Constraints
  • List rows, not card grids · D-pad navigation only
  • Min focus target 88px
  • No card walls · no hero rails · no "Continue Watching" if we can't track it
C.07

Focus

scale(1.06) + 4px yellow ring + drop shadow. Same visual on every TV platform, different engines drawing it.
C.08

Top navigation

Glass tabs + search affordance. Backdrop-blur where the runtime supports it; flat fallback where it doesn't.
C.09

Action menus

File-actions overlay. Same composition rules as the mobile bottom-sheet — different chrome, identical IA.
C.10

Player chrome

Scrubber, audio/subtitle pickers, info overlay. Player engine is platform-specific (AVPlayer / libVLC / Roku / HTML5).
C.11

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. Per-platform engine, surface, player-ownership and token-mapping breakdown lives in the platform repos.
C.12

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 + preview/tv-shell.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.
Native consumers · out-of-treenot shipped from this repo
iOS
Apple TV · tvOS app
putio-ios (separate repo)
native
AT
Android TV / Google TV
putio-android (separate repo)
native
RK
Roku channel
putio-roku (separate repo)
native
SM
Smart-TV web · Tizen / webOS
putio-tv-web (separate repo)
native