Scope of this version
Web version (desktop + mobile web) of the ShortDrama bootstrap. Tokens and layout rules below come from competitor research done 2026-09-23 (research/): real CSS fetched from 6 competitor sites, plus live-browser screenshots of ReelShort, DramaBox, GoodShort, NetShort at 1440×900 and 390×844. There is still no mood board of our own and no real brand name — "ShortDrama" is a placeholder. Every rule cites the research file it came from; anything the research did not observe is marked as our own choice.
Voice & Copy
Short, punchy English, imperative CTAs: "Play", "Continue EP 5", "Unlock for 50 coins", "Watch ad to unlock". No emoji in UI copy or controls — the coin is an SVG glyph, not 🪙. Meta always pairs a number with a unit: "1.2M views", "67 episodes". Episode labels are "EP 13", never a bare "13" outside the numbered grid tile. Price lines show the currency symbol and two decimals: "$19.99".
Colour
Two warm hues, strictly separated:
accent-600(hot pink-red) = the brand and the action colour: Play, Unlock-with-coins CTA, Hot/New badge, current-episode tile, active-nav underline. This matches the category norm (5 of 7 competitors,research/visual-signals.md).vip-gold= VIP and coins only: VIP plan cards, coin glyph and balance, VIP badge. Never on a Play/Unlock button. Every competitor keeps VIP gold separate from its brand red.
Don't use accent-600 for small text on surface-000 in dark theme (4.29:1, fails AA). The active nav item keeps its ink-900 label and shows a 2px accent underline. Build every page from surface-000 (page and player letterbox) → surface-100 (nav, panels, sheet) → surface-200 (chips, episode tiles, coin packs). Dark is the default theme. Light theme exists and passes WCAG, but no competitor ships a light product UI (DramaBox is light only on its SEO shell).
Typography
Plus Jakarta Sans for everything, with Noto Sans SC / PingFang SC as the fallback, because titles and actor names often come from Chinese-language sources (every competitor ships a CJK fallback). Never Inter or Roboto. display-xl is used only for the desktop hero title. title-m is for drama titles and section headers ("Trending Now", "New Release"). label is for anything tappable.
Layout
- Desktop (≥1024px): top nav + centred content up to
content-max(1280px). Home rails scroll horizontally, withposter-w176px at 3:4. - Mobile web (<768px): same top nav condensed to hamburger + logo + search + coin/avatar. Rails at 132px. Gutter
space-4. - Posters are always 3:4 (
poster-aspect). v1 used 2:3, but every measured competitor sits at 0.72–0.77. - Never ship a non-responsive layout (GoodShort renders desktop DOM at 390px and forces horizontal scroll). That is the explicit anti-pattern.
Navigation
Top nav only; there is no bottom tab bar anywhere sitewide (0 of 4 live-checked competitors use one).
- Desktop: logo · Home · Genres · (spacer) · search · history · language · coin pill · avatar/Login · "Top Up" CTA.
- Mobile: hamburger · logo · (spacer) · search · coin pill.
The player is a pushed page with a back button. Language selection stays in the primary nav, as on every competitor.
Player
- Desktop: 9:16 video centred in a full-height
surface-000letterbox column. Aside-panel-w(416px) panel sits on the right with breadcrumb, "EP 1 · Title", synopsis with "More", tag chips, a like/save/share row, and the EpisodeGrid (ReelShort/DramaBox/NetShort,research/live-player-nav.md). - Mobile: video edge-to-edge full width. Below it: title, then a collapsed "All episodes" row that expands into the grid.
A persistent app-download nudge is allowed, but it must be non-blocking (a dismissible corner card). It never covers the video.
Paywall
Tapping a locked episode never hits a login wall first: free episodes play without an account (research/monetization.md). A locked episode opens UnlockSheet, a centred modal on desktop and a bottom sheet on mobile. It stacks, top to bottom:
1. Price + balance header.
2. Unlock for N coins (primary).
3. VIP plan cards (weekly / yearly) with a feature checklist.
4. Coin-pack grid.
5. Watch ad to unlock (secondary, capped per day).
6. Payment-method row (Apple Pay / Google Pay / Card).
Checkout happens on the web. Never "download the app to unlock" as the only path.
Default free block: the first 8 episodes are free (our choice inside the observed 5–15 range; tune with data).
Iconography
Outline icons, 1.75–2px stroke. Lock glyph on locked episode tiles (top-right corner, episode number still visible). Play triangle on hero and poster hover. Coin = circle glyph filled vip-gold.
Imagery
Poster art is supplied by content partners and already has the title baked in (see the ReelShort rails). Never overlay our own title text on a poster in rails; the title goes below or appears on hover. A top gradient scrim (black 50% → transparent) is allowed only to keep a corner badge legible. Previews in this DS use flat colour blocks, never redrawn real show art.
States
- Episode tiles:
default(surface-200),current(accent-tinted fill + playing bars glyph),watched(ink-600 number),locked(lock glyph, number stays readable). - Insufficient coins: the Unlock CTA stays enabled but relabels to "Top up to unlock" and scrolls the sheet to the coin packs. A disabled grey button is a dead end.
- Empty history is an illustration plus a "Browse trending" CTA, never "No data".
Known Gaps
- Not designed: profile/settings, post-purchase success toast, empty-results search state, email-code entry step after "Email me a sign-in code".
- Light theme:
vip-goldwas darkened to#946508for AA text, so the coin glyph reads brown in light theme. If that's unwanted, split a non-textcoin-filltoken (glyphs don't need 4.5:1). - Unverified: logged-in purchase flow (no competitor login was performed during research).
- No rank #1/#2/#3 badge. ReelShort's CSS has the classes, but no ranking badge was visible on any of the 4 sites checked live, so it's deferred until we see it rendered.
Actions
ButtonPrimary
Pill CTA. primary (accent fill) is the one main action per view; secondary (outline) is the free alternative.
When to use: primary: Play (hero), Unlock for N coins / Top up to unlock (UnlockSheet), Top Up (nav, sm). secondary: Watch ad to unlock.
Anatomy: Pill (height --cta-height, sm = 40px) · optional leading glyph (play / lock / ad) · .label.
Variants & states: primary · secondary · block (full width) · sm. Not enough coins → relabel "Top up to unlock", never disabled (a disabled button is a dead end in the money path).
Tokens used: --primary, --on-accent, --border-soft, --ink-900, --cta-height, --radius-pill.
Consumer provides: label, variant, icon?, onPress.
Do: One primary per view (the nav's Top Up sm is the sole exception).
Don't: Don't fill a button with vip-gold — gold marks VIP offers, not actions.
Commerce
AuthSheet
Sign-in sheet shown only at the first paid action — never before a free episode.
When to use: User taps Pay / Unlock / Subscribe / Watch ad while signed out (research/monetization.md §4: anonymous viewing of the free block, account at first monetized action). Also from the avatar in TopNav.
Anatomy: Title "Sign in to keep your coins" + close · one-line reason · "Continue with Google" · "Continue with Apple" · "or" divider · TextField email · primary "Email me a sign-in code" · Terms/Privacy line.
Variants & states: modal (440px, desktop) · bottom (mobile sheet). After success, return the user to the exact action they started (the pending unlock/purchase), not to Home.
Tokens used: .sd-sheet shell tokens, --ink-900/--surface-000 (light buttons), --primary, --border-soft.
Consumer provides: providers[], onProvider, onEmail, pendingAction.
Do: Text-only provider buttons until we have the official brand assets (never redraw Google/Apple marks from memory). Providers are our own choice — research did not capture any competitor login flow.
Don't: Don't ask for a password — email code keeps sign-up to one field. Don't gate free episodes behind this sheet.
UnlockSheet
The paywall: shown when the viewer opens a locked episode. Centered modal on desktop, bottom sheet on mobile.
When to use: Tap on a locked EpisodeGrid tile, or auto-next reaching a locked episode. Never before a free episode; no login wall in front of it.
Anatomy (top→bottom): price + balance header with close · "Unlock EP N to keep watching" · primary CTA · VIP plan cards (2, vip-surface, selected = gold ring, BEST VALUE badge) + perk checklist · divider · coin-pack grid (4 desktop / 2 mobile, bonus Badge) · secondary "Watch ad to unlock · N left today" · payment row (Apple Pay / Google Pay / Card).
Variants & states: modal (720px) · bottom (full width, max 92vh, scrolls). CTA: balance ≥ price → "Unlock for N coins" (lock glyph); balance < price → "Top up to unlock" (scrolls to packs) — never a disabled button.
Tokens used: --surface-100, --surface-200, --vip-gold, --vip-surface, --on-vip, --primary, --radius-md, --shadow-card.
Consumer provides: price, balance, plans[], packs[], adsLeft, paymentMethods[], onUnlock/onSubscribe/onBuyPack/onWatchAd.
Do: Keep VIP in gold and the Unlock CTA in pink-red — two different offers, two colours (visual-signals.md).
Don't: Don't make "Download the app" the only unlock path (GoodShort/DramaBox dead-ends, live-player-nav.md). Prices shown here are placeholders — set real ones per business decision (open-questions #6).
Content
EpisodeCard
Portrait 3:4 poster tile for rails and grids — the core content unit on Home and Genre pages.
When to use: Home rails (Continue Watching, Trending Now, New Release, genre rails), Genre grid.
Anatomy: Poster (--poster-aspect 3:4, width --poster-w 176px desktop / 132px mobile, radius-md, top scrim) · optional Badge top-left · hover play glyph · optional progress bar bottom (Continue Watching) · title (label, 2-line clamp) + caption meta below.
Variants & states: default · badged (HOT/NEW) · in-progress (accent progress bar + "EP 12 of 70") · hover (play glyph).
Tokens used: --poster-w, --poster-aspect, --radius-md, --accent-600, --space-2.
Consumer provides: posterSrc, title, meta, badge?, progress?, href.
Do: Use 3:4 — every measured competitor sits at 0.72–0.77 (research/visual-signals.md); v1's 2:3 is retired.
Don't: Don't overlay our own title on the art — partner posters already carry it; title goes under the poster.
EpisodeGrid
Numbered episode picker — the player's navigation and the visible map of what is free vs locked.
When to use: Desktop player side panel (6 columns in side-panel-w); mobile player inside the expandable "All episodes" row.
Anatomy: Range tabs (1 – 50, 51 – N) + "All episodes ›" · grid of 44px tiles, one per episode.
Variants & states (per tile): default (unwatched, free) · current (fill primary, playing-bars glyph) · watched (ink-600 number) · locked (lock glyph top-right, number stays readable). Clicking a locked tile opens UnlockSheet.
Tokens used: --episode-tile, --surface-200, --primary, --on-accent, --ink-600, --space-2.
Consumer provides: episodes[]{n, state}, current, range, onSelect.
Do: Show locked episodes greyed-with-lock, never hide them — universal across the 4 live-checked sites.
Don't: Don't assume "first N free" in code — ReelShort scatters free teaser episodes (live-player-nav.md); state is per episode from the API.
HeroBanner
Home's featured-title banner: full-bleed art, title, tags, 2-line synopsis, Play CTA, thumbnail strip to switch titles.
When to use: Top of Home only, 2–5 featured titles (web-screenflow.md §1 — 6/6 competitors open Home with this).
Anatomy: Art background + left→right and bottom scrims · body (display-xl, Badge HOT + static Chips, body synopsis clamped to 2 lines, caption meta, ButtonPrimary Play) · thumbnail strip bottom-right (3:4, selected = 2px ring).
Variants & states: desktop (overlay text on art) · mobile (art 4:5 on top, text below on surface-000, display-l).
Tokens used: --poster-aspect, --radius-md, --space-7, --primary, .display-xl/.display-l, .body, .caption.
Consumer provides: items[]{title, art, tags, synopsis, meta}, activeIndex, onPlay.
Do: Keep the scrim — hero text sits on arbitrary art; the text colour is fixed light, not theme ink.
Don't: Don't autoplay trailer audio in the hero.
Navigation
SearchField
Pill search input — opens the Search page from the nav search icon.
When to use: Top of WEB-SEARCH-01 (autofocused). Not inlined in the desktop nav (nav keeps an icon, like every competitor).
Anatomy: Search glyph · text input (body) · clear button (only when non-empty).
Variants & states: empty (placeholder ink-600) · focused (1.5px ink-900 ring) · with query (clear button).
Tokens used: --surface-200, --ink-900, --ink-600, --radius-pill, --space-4.
Consumer provides: value, onChange, onClear, onSubmit.
Do: Search titles, actors and genres from one field; show recent + trending searches before typing.
Don't: Don't use accent for the focus ring — focus is ink-900, accent is reserved for actions.
Selection
Chip
Small pill for a genre — selectable (Discover filter) or display-only (Drama detail tags).
When to use: Discover's filter row (tappable, single-select) and Drama detail's genre tag row (display-only, not tappable).
Anatomy: Pill container (height --chip-height) · centered .label, no icon.
Variants & states: selected (fill --primary, label --on-accent) · unselected (fill --surface-200, label --ink-900) · display-only (same visual as unselected, but no press state — used on Drama detail).
Tokens used: --primary, --surface-200, --ink-900, --on-accent, --chip-height, --radius-pill.
Consumer provides: label, selected?, interactive (bool).
Do: Use single-select for the Discover filter row — one chip selected at a time, "All" included as a real option.
Don't: Don't make the Drama-detail genre chips look pressable (no press-state styling) — they're metadata, not a filter there.
TextField
Labelled single-line input — currently only the email field on AuthSheet.
When to use: Email sign-in. Search has its own SearchField (pill, icon, clear).
Anatomy: label above · 48px input on surface-200, radius-sm · optional message line below.
Variants & states: default · focus (1.5px ink-900 ring) · error (1.5px accent-600 ring + message in ink-900 — accent text is 4.29:1 on dark, below AA).
Tokens used: --surface-200, --ink-900, --ink-600, --accent-600, --radius-sm.
Consumer provides: label, value, placeholder, error?, type.
Do: Keep the label visible above the field (placeholder is an example, not the label).
Don't: Don't colour the error message red text — the ring carries the colour, the message carries the words.
Status
Badge
Tiny status pill overlaid on a poster or CTA — functional signal, not decoration.
When to use: Hot/New on a poster corner (Discover, Drama-detail episode grid) · coin-cost badge on a locked episode tile.
Anatomy: Small pill (height --badge-height) · label only (Hot, New, or a coin count like 12).
Variants & states: status (HOT/NEW, bonus +25% on coin packs — fill --accent-600, label --on-accent) · vip (VIP, BEST VALUE — fill --vip-gold, label --on-vip).
Tokens used: --accent-600, --on-accent, --vip-gold, --on-vip, --badge-height, --radius-pill.
Consumer provides: kind (hot|new|coin), value? (coin count).
Do: Keep this the only place besides ButtonPrimary that fills solid accent-600 — it must stay rare enough to read as a signal.
Don't: Don't add a third badge colour (e.g. green "Free today") without a documented reason; no rank #1/#2/#3 badge until observed live (README §Known Gaps).
CoinPill
Coin balance pill — keeps the balance visible before any unlock moment.
When to use: TopNav (desktop + mobile), UnlockSheet header (price/balance).
Anatomy: Gold coin glyph (vip-gold) + balance (label) on surface-200 pill.
Variants & states: nav (height --coin-pill-height) · zero (shows 0, still visible — never hide the pill at zero).
Tokens used: --vip-gold, --surface-200, --ink-900, --coin-pill-height, --radius-pill.
Consumer provides: balance, onPress (opens coin store — WEB-STORE-01, not yet designed).
Do: Coin glyph is always vip-gold (v1 used accent — changed after research/visual-signals.md).
Don't: Don't render a bare number without the glyph.