Design system

Loopd UI kit

The maintained Loopd design system, on one public page. Use it to check a component before you build a variant of it, to copy a token name, and to see how each piece behaves in dark mode. Resize to 390px: everything here is expected to hold.

Next.js 16 · React 19Tailwind v4 · shadcn/uiBricolage Grotesquelucide · motion · vaul · sonner
Foundations

Color

Tokens live in src/app/globals.css. Light uses :root; dark uses .dark. @theme inline exposes them to Tailwind. The two chips below each row are the light and the dark value; a faded second chip means .dark does not override that token. Light and dark use the same semantic roles across the component library.

How to read this

Left chip = :root · right chip = .dark

LightDarkInherited

Click the copy icon to put var(--color-*) on your clipboard. Toggle the theme in the header to see every component below re-render.

Surfaces

14 tokens

The page ground is never white. Cards float on #f7f6fe in white with a 20–24px radius; muted fills stand in for borders on inputs and secondary pills.

--color-background

#f7f6fe → #08061e

bg-background Page ground behind every app and marketing screen.

--color-card

#ffffff → #100c30

bg-card Every card, panel, table shell and popover body.

--color-card-foreground

#030119 → #e8e6fe

text-card-foreground Ink on cards.

--color-popover

#ffffff → #100c30

bg-popover Notifications panel, avatar menu, select and tooltip surfaces.

--color-popover-foreground

#030119 → #e8e6fe

text-popover-foreground Ink inside popovers.

--color-muted

#f0effb → #171240

bg-muted Borderless input fills, add tiles, eyebrow pills, skeleton base.

--color-muted-foreground

#6b6488 → #8b82c0

text-muted-foreground Micro labels and helper text. Not placeholders: those have their own token.

--color-placeholder

#807a9c → #736b9e

placeholder:text-placeholder Placeholder text only, and 25% less contrast than muted-foreground so an empty field never reads as filled. Set once by Input/Textarea/SelectTrigger and the base ::placeholder rule: never respecify it per field.

--color-secondary

#eae8fe → #201a52

bg-secondary Gray pill buttons, active nav tab background, segmented chips.

--color-secondary-foreground

#030119 → #e8e6fe

text-secondary-foreground Ink on gray pills.

--color-accent

#eae8fe → #201a52

bg-accent Hover fill for ghost buttons, menu items and table rows.

--color-accent-foreground

#030119 → #e8e6fe

text-accent-foreground Ink on hover fills.

--color-surface-dark

#030119 → #241d68

bg-surface-dark Inset dark marketing sections (How it works, Active campaigns, For brands).

--color-surface-dark-foreground

#f7f7f8 → #f7f7f8

text-surface-dark-foreground Ink on the dark marketing sections.

Ink and lines

5 tokens

Two text colors carry almost the whole product: near-black for anything you read, #5c5578 for anything you skim.

--color-foreground

#030119 → #e8e6fe

text-foreground Headings, values, icons: the default ink.

--color-content-secondary

#5c5578 → #8b82c0

text-content-secondary Subtitles, meta rows ('2d ago • Per View'), body copy under a heading.

--color-border

#e2dff6 → #2c2668

border-border Hairlines: card outlines, table rows, section dividers.

--color-input

#e2dff6 → #2c2668

border-input Outlined control borders (select trigger, checkbox, OTP slots).

--color-ring

#1d09f6 → #6e62f9

ring-ring Focus ring. Text fields swap it for a 2px solid near-black border.

Actions and brand

6 tokens

Black pills are the product's primary action; brand-500 #1d09f6 is the brand; brand-600 #1807ca is reserved for links, switches and the lifecycle stepper. The app is otherwise monochrome.

--color-primary

#030119 → #e8e6fe

bg-primary Primary CTA pill (Continue, Join campaign, Submit post, Cash out).

--color-primary-foreground

#ffffff → #030119

text-primary-foreground Label on the primary pill.

--color-brand

#1d09f6 → #4535f8

bg-brand Loopd blue: the mark, hero CTAs, FAQ toggles, footer gradient, cookie banner.

--color-brand-foreground

#ffffff → #ffffff

text-brand-foreground Label on brand-blue CTAs.

--color-action

#1807ca → #978efb

bg-action Switch ON, campaign lifecycle stepper, inline links ('Let us know.').

--color-action-foreground

#ffffff → #030119

text-action-foreground Label on action-blue fills.

Semantic

7 tokens

Status colors appear as soft tinted pills far more often than as solid fills: 'Earning $2.00 per 1,000 views' and 'Added to wallet' are green-on-green.

--color-success

#087d3e → #0cb85b

text-success Approved submissions, 'Added to wallet', earnings badges.

--color-success-foreground

#ffffff → #030119

text-success-foreground Ink on solid green.

--color-warning

#f99c00 → #fcbb00

text-warning Pending / in-review states, '25% less earnings' connect badge.

--color-warning-foreground

#161616 → #030119

text-warning-foreground Ink on solid amber.

--color-destructive

#cf2533 → #ff6568

text-destructive Rejected clips, field errors, Close account, Delete.

--color-destructive-foreground

#ffffff → #ffffff

text-destructive-foreground Ink on solid red.

--color-info

#1807ca → #978efb

text-info Informational banners and info-circle tooltips.

Charts

5 tokens

Used by the brand dashboard only. No dark overrides: these five read on both grounds.

--color-chart-1

#5dc16b · inherits

bg-chart-1 Series 1: views.

--color-chart-2

#1d09f6 · inherits

bg-chart-2 Series 2: spend.

--color-chart-3

#f99c00 · inherits

bg-chart-3 Series 3: pending.

--color-chart-4

#ac4bff · inherits

bg-chart-4 Series 4: posts.

--color-chart-5

#ff2357 · inherits

bg-chart-5 Series 5: rejected.

Sidebar

8 tokens

Inherited from shadcn. Loopd has no sidebar; these are kept so shadcn blocks drop in without a token rewrite (the settings anchor nav uses them).

--color-sidebar

#ffffff → #100c30

bg-sidebar Sidebar surface.

--color-sidebar-foreground

#030119 → #e8e6fe

text-sidebar-foreground Sidebar ink.

--color-sidebar-primary

#030119 → #e8e6fe

bg-sidebar-primary Selected sidebar item.

--color-sidebar-primary-foreground

#ffffff → #030119

text-sidebar-primary-foreground Ink on the selected item.

--color-sidebar-accent

#f0effb → #201a52

bg-sidebar-accent Sidebar hover fill.

--color-sidebar-accent-foreground

#030119 → #e8e6fe

text-sidebar-accent-foreground Ink on sidebar hover.

--color-sidebar-border

#e2dff6 → #2c2668

border-sidebar-border Sidebar hairline.

--color-sidebar-ring

#1d09f6 → #6e62f9

ring-sidebar-ring Sidebar focus ring.

Foundations

Typography, radius, shadow, space

Bricolage Grotesque carries the whole product: variable on the web, four static weights (400/500/600/700) in the native apps. The marketing site heads big and tight; the app runs 2–4px smaller and leans on weight rather than size. Numbers are always tabular so balances don't jitter as they refresh.

Editorial scale

Marketing pages, auth and the onboarding wizard

Three steps to your first payout

Display / H1

36px / 600 / -0.9px

Marketing section heads, wizard H1 ('Make your profile').

See the rate. Post the clip.

H2

32px / 600 / -0.02em

Secondary marketing heads, mobile page titles ('Discover campaigns').

Find one worth clipping

H3

20px / 600 / -0.01em

Card group heads, dialog titles, FAQ questions.

Loopd is the clipping platform that pays creators per verified view, on every clip they post.

Body

16px / 24px / 400

Marketing body copy and page subtitles.

CAMPAIGNS ON LOOPD

Eyebrow

11px / 600 / uppercase / 0.08em

Section chips above every marketing heading; `eyebrow` utility.

Product scale

Signed-in app: Home, Campaigns, Add Clips, Earnings

Discover campaigns

Page title

24px / 700 desktop · 30–32px / 700 mobile

PageHeader title. Mobile bumps a step; desktop stays tight.

My submissions (12)

Section heading

18px / 500

Card section heads inside the campaign modal and settings.

Outlast the Night, Win $250,000

Card title

13–15px / 600

Campaign titles, list-row titles, stat labels.

Your views start counting from the moment you submit.

Body / value

13–14px / 400

The app's default body size: smaller than the marketing site.

16h ago • Per View

Meta

12px / 400

Timestamps, secondary rows, helper text under fields.

PER 1M VIEWS

Micro label

9–11px / 600 / uppercase / 0.04–0.08em

PER 1M VIEWS, LIVE RATE, PAID OUT, CLIPPER, field labels.

$1,500

Numeric

22–33px / 700–800 / tabular-nums

Payout figures, balances, stat values. Always tabular.

Weights

Variable 200–800; these four are the ones the product uses

Get views

Regular 400

Body copy, values, table cells.

Get views

Medium 500

Section heads, buttons on marketing.

Get views

Semibold 600

App headings, card titles, CTAs.

Get views

Bold 700

Page titles, payouts, big numbers.

Radius

--radius is 0.75rem; the rest are derived in @theme inline

6px

10px

12px

16px

20px

24px

36px

9999px

rounded-sm
6px
Checkbox, tiny chips, table cell affordances.
rounded-md
10px
Birthday OTP slots, menu items, small icon wells.
rounded-lg
12px
--radius base. Alerts, nav active tab, toasts.
rounded-xl
16px
Stat cards, banner cards, link rows, inputs.
rounded-2xl
20px
Section cards, lifecycle card, stats strip.
rounded-3xl
24px
The default card. Campaign cards, empty states, popovers.
rounded-4xl
36px
Onboarding wizard panel, campaign modal on desktop.
rounded-full
9999px
Every button, chip, badge, avatar and tab bar. No exceptions.

Elevation

Two shadows carry the entire product

shadow-card

Resting surface

shadow-float

Floats over content

border only

Dense / nested cards

shadow-card
0 1px 2px rgb(22 22 22 / .04), 0 8px 24px -12px rgb(22 22 22 / .12)
Cards and panels. Near-invisible by design: Loopd is a flat product.
shadow-float
0 12px 40px -12px rgb(22 22 22 / .18)
Things that hover over content: bottom tab bar, marketing pill nav, cookie banner.
shadow-sm
Tailwind default
Dense surfaces where shadow-card is too soft (table shell, inline pills).
shadow-none
none
Anything already separated by a 1px border. Never stack a border and a shadow-card.

Spacing and measure

4px base grid (Tailwind default)

Page gutter
16px mobile · 24–32px desktop
px-4 md:px-6 on every route container.
Content column
560 / 900 / 1040px
Settings & auth / Home & Campaigns / Add Clips & Earnings.
Card padding
20px mobile · 24px desktop
p-5 md:p-6: the standard card interior.
Card gap
16–20px
space-y-4 md:space-y-5 between stacked cards.
Section gap
32px
space-y-8 between page sections.
Control height
36 / 44 / 56px
Button sm / lg / xl. Inputs are 48–56px on mobile.
Bottom tab clearance
110px
pb-28 on mobile lists so the floating tab bar never hides a card.

Rule of thumb: containers use px-4 md:px-6, cards use p-5 md:p-6, stacks use space-y-4 inside a card and space-y-8 between page sections.

Primitives

Buttons

Every button is a pill. Loopd never uses a squared button anywhere in the product, and it presses in on tap (active:scale-[0.98]) so a touch feels acknowledged before the network answers. Disabled drops to 40% opacity and kills pointer events: used constantly, because most CTAs stay disabled until a field validates.

Variants

11 variants: import { Button } from '@/components/ui/button'

default

Black pill. Every primary action in the app: Continue, Join campaign, Submit post, Cash out.

brand

Loopd blue #1d09f6 (brand-500). Marketing CTAs only: Start Clipping, Accept cookies, Launch a campaign.

action

#1807ca (brand-600). Reserved for in-product blue affordances (rare: mostly links and toggles).

outline

White pill + 1px border. Connect an account, Cancel in dialogs.

outline-brand

Loopd-blue border on transparent. Reject on the cookie banner, secondary marketing CTA.

soft

Light gray fill. Replace picture, Settings, secondary rows.

secondary

One step darker than soft. Segmented chips and active sort pills.

ghost

No fill until hover. Icon buttons in headers, table row actions.

soft-destructive

Light red fill, red text. Every destructive TRIGGER at rest: Remove, Delete, Disconnect, Revoke, Delete account.

destructive

Solid red. Only the confirm button inside a ConfirmDialog: never at rest on a page or card.

link

Inline text action. 'Let us know.', 'Show more', legal links.

Sizes

xs 24 · sm 32 · default 36 · lg 44 · xl 56

xs
sm
default
lg
xl
icon

States and patterns

What the mock actually renders while a mutation is in flight

Pending

Disable the button and swap the label: never show a spinner on top of live text.

Full-width CTA

Sheets, cards and every mobile footer.

As a link

asChild keeps the pill but renders an <a>.

Icon + label pair

Icons sit left; 16px, lucide, 1.5px stroke.

Destructive: trigger vs confirm

Soft red is the trigger sitting in a page, card or row. Solid red is the confirming action inside the ConfirmDialog it opens: the user meets it exactly once, at the point of no return.

Primitives

Form controls

Two field styles ship: the shadcn outlined default for dense forms (brand dashboard, filters) and the filled Loopd field for anything a creator types on their phone. Focus is a 2px near-black border on the filled style: no blue glow. Errors color the label, the border and the helper line at once.

Text fields

Filled = the Loopd default; outlined = dense/brand surfaces

Filled (default)

FILLED_FIELD: 48px, radius 16px, no border until focus.

With leading icon

3–20 characters. Letters, numbers, underscores and dots.

Error

That username is taken. Pick a different one.

Read-only / disabled

Email is never editable: the settings row drops its pencil icon.

Outlined (shadcn default)
Search

Placeholder vs. entered text

--color-placeholder · #807a9c light / #736b9e dark

Empty
Filled

Placeholders carry 25% less contrast than text-muted-foreground (3.5:1 on bg-card, 3.1:1 on bg-muted) so the two columns above never read the same at a glance: an empty field must not look answered. The colour is set once, by Input, Textarea, SelectTrigger and the base ::placeholder rule. Never write placeholder:text-muted-foreground in feature code: it is the old value and it wins over all three.

Select, textarea and toggles

Select: location
Select: language
Textarea

Support form. Submit stays disabled until a message is typed.

Switch: notification prefs

New campaigns

Email me when a campaign goes live.

Campaign updates

Approvals, rejections and payout news.

ON uses bg-action (#1807ca): the one place blue appears in settings.

Checkbox and radio

Segmented inputs

input-otp: the same primitive powers the email code and the birthday gate

Email code: 6 digits

Dev code is 123456. Value: "Not set"

Birthday: MM / DD / YYYY

8 boxes, literal separators, letter placeholders. Value: "Not set"

Patterns

Cards, badges and stats

One card recipe repeats everywhere: white, rounded-3xl, p-5 on mobile and p-6 on desktop, shadow-card, no border. Nested cards drop the shadow and take a hairline instead. Status is always a soft tinted pill: solid fills are reserved for buttons.

Card recipes

Default card

rounded-3xl bg-card p-5 md:p-6 shadow-card

Nested / dense

Hairline instead of a shadow. Used inside modals and settings sections.

Dark section

Inset marketing bands: How it works, Active campaigns, For brands.

PageHeader

src/components/shared/page-header.tsx

Campaigns on Loopd

Discover campaigns

Join a campaign, follow the brief, and get paid for the views your clips generate.

Earnings

Views and pending earnings update every hour.

CampaignCard

src/components/shared/campaign-card.tsx: the most reused component in the product

Kova

1mo ago • Per View

Outlast the Night, Win $250,000

$1,500

Per 1M views
VerifiedBadge
Kova

This account is verified with funds deposited in Loopd.

PlatformIconRow
Instagram, TikTok, YouTube Shorts, X

Canonical order: Instagram, TikTok, YouTube Shorts, X.

The rate tooltip always reads “This rate can move while the campaign runs.”. Pass href for the public board and onSelect for the in-app grid, which opens the campaign modal instead of navigating.

Badges and status pills

src/components/shared/status-badge.tsx

Badge variants
DefaultSecondaryOutlineDestructiveGhost
Campaign lifecycle
DraftPending approvalScheduledActiveFinalizingPausedCompleted

One hue per stage, in lifecycle order.

Submission status
VerifyingPendingApprovedRejected

'Verifying' pulses while the mock runs its checks.

Ledger status
Added to walletPendingPaid out

Ledger rows override the label.

Marketing eyebrow
Campaigns on LoopdHow it works

`eyebrow` utility class in globals.css

Earning badge
Earning $1.80 per 1,000 views

Green-on-green, on every joined campaign header.

StatCard, StatRow and Money

src/components/shared/stat-card.tsx

Available
$488.92
Pending
$200.02
Paid out
$12,050.06
Views
1.8m
Money
default $488.92whole $1,500compact $12.1ksigned +$214.60negative -$500.00
Campaign stats strip
Ends

6d

Language

English

Pay type

Per View

Live rate

$2.00 cpm

Budget paid out47.36%

Lifecycle progress uses bg-action; upcoming steps stay on bg-border.

Alerts and banners

EmptyState

src/components/shared/empty-state.tsx

Nothing new

Campaign and payout updates land here.

No available earnings

Submit a clip, clear 5,000 views, and the money lands here.

Optional icon well, one bold line, one gray sentence, at most one black pill. Never two CTAs, never an illustration. Use variant="plain" when the empty state already sits inside a card.

Patterns

Overlays and feedback

Three overlay shapes. A decision is a ConfirmDialog, centered at every breakpoint. Content flows such as briefs, forms and wizards use a centered dialog on md+ and a vaul bottom sheet below it. ResponsiveDialog picks the container. A transient status step with no controls passes variant="centered" so it never slides up from the bottom edge. Many Loopd dialogs deliberately have no close X and dismiss with the footer button or the overlay; where one is shown it is the shared close pill. Everything asynchronous answers with a sonner toast at the top of the screen.

Tooltips and popovers

Radix, 200ms delay, no arrow

Tooltip
Locked

Info circles sit next to every rate and balance label.

Popover

Notifications panel and avatar menu use this surface.

Dialog and drawer

Dialog on md+ · Drawer (vaul) below 768px: see ResponsiveDialog

Sheets pin their primary CTA to the bottom with safe-bottom so the home indicator never covers it. The overlay is black/50 in both cases.

ConfirmDialog

src/components/shared/confirm-dialog.tsx: centered at every breakpoint

Every “are you sure?” goes through this one. Radix AlertDialog underneath, so it gets role="alertdialog", focus trapped on the two buttons and no dismiss-on-outside-click. It never closes itself: the caller closes on success, which lets an in-flight request hold it open with pending.

ResponsiveDialog

src/components/shared/responsive-dialog.tsx: dialog on md+, drawer below

Narrow this window past 768px and reopen it: the same call renders a bottom sheet. Every content or task flow goes through this component rather than picking one by hand; a two-button question uses ConfirmDialog instead.

Scroll chrome and the close pill

globals.css subtle-scrollbar · overlayCloseButton in ui/dialog.tsx

A default scrollbar is a grey rectangle, and inside a rounded-3xl surface it draws a hard square edge straight across the corner. Every overlay surface and everything inside it therefore gets subtle-scrollbar automatically: transparent track, a pill thumb held 4px off the edge, and the track inset 14px at both ends so the thumb can never reach a corner. The surface itself is overflow-hidden, so the radius always wins. Nothing in feature code has to opt in; apply the utility by hand only for a scroller that sits on a rounded card outside an overlay. no-scrollbar is still there for chip rails, and still beats this: but content that scrolls should say so.

The close control is one shared pill: 36px of grey with a 44px tap target painted by a transparent ::before, its own fill so it stays readable over a header image or over content sliding underneath, and a 3px ring-ring/40 on keyboard focus. Many Loopd dialogs omit the close button with showCloseButton={false} and dismiss through the footer button or the overlay; the mobile sheet always closes by drag or overlay.

Toasts

sonner, richColors, top-center

Anything the API throws goes through toastApiError(error) unless it belongs to a single field: field errors render inline, never as a toast.

Error copy

Every ApiErrorCode and the surface that renders it

CodeMessageSurface
UNAUTHENTICATEDYou need to be logged in to do that.Redirect to /login?redirectTo=
OTP_INVALIDThat code isn't right. Check your email and try again.Inline under the OTP boxes
OTP_EXPIREDThat code has expired. Request a new one.Inline + 'Resend code' link
UNDERAGEYou must be 18 or older to use Loopd.Blocking dialog on /birthday
REGION_UNSUPPORTEDLoopd isn't available in your country yet.Full-page block
USERNAME_TAKENThis username already exists. Please choose a new one.Inline field error
USERNAME_INVALIDUsernames are 3-20 characters and can use letters, numbers, underscores and dots.Inline field error
ACCOUNT_REQUIREDConnect a social account before submitting clips.Banner in the submit sheet
SOCIAL_CONNECT_FAILEDWe couldn't connect that account. Please try again.Toast
SOCIAL_ALREADY_LINKEDThat account is already linked to your profile.Toast
SOCIAL_CODE_INVALIDThat bio code isn't right. Check your profile and try again.Inline banner in the connect dialog
PHONE_REQUIREDVerify your phone number to join new campaigns.Banner + phone verify dialog
PHONE_CODE_INVALIDThat code isn't right. Check your messages and try again.Inline under the code boxes
CAMPAIGN_LOCKEDThis campaign is locked for submissions.Modal banner, Join disabled
CAMPAIGN_NOT_FOUNDWe couldn't find that campaign.Empty state on the route
NOT_JOINEDJoin the campaign before submitting a clip.Banner in the submit sheet
INVALID_URLThat doesn't look like a post link.Inline field error
UNSUPPORTED_URLThat platform isn't supported yet.Inline field error
PLATFORM_NOT_ALLOWEDThis campaign doesn't accept posts from that platform.Inline field error
DUPLICATE_POSTThis post has already been submitted.Inline field error
DAILY_LIMITYou've hit the 5 posts per account, per day limit. Try again in 24 hours.Banner in the submit sheet
NO_PAYOUT_METHODAdd a payout method before cashing out.Cash-out sheet CTA
PAYOUT_METHOD_INVALIDCheck that address and try again.Inline under the address field
PAYOUT_METHOD_IN_USEA payout is on its way to this method. You can remove it once that has been paid.Toast on the remove confirmation
MIN_CASHOUTYou need at least $10 available to cash out.Inline under the amount
CASHOUT_COOLDOWNCash outs can be requested once every 7 days.Disabled CTA + helper line
INSUFFICIENT_FUNDSThat's more than your available balance.Inline under the amount
ASSET_TYPE_UNSUPPORTEDThat file type isn't supported. Upload MP4, MOV, PNG or JPG.Rejected before upload, inline on the queue row
ASSET_TOO_LARGEFiles have to be 512 MB or smaller.Rejected before upload, inline on the queue row
ASSET_DUPLICATE_NAMEA file with that name is already in this folder.Failed queue row / inline on rename
ASSET_UPLOAD_FAILEDThat upload didn't finish. Try again.Failed queue row + Retry
ASSET_QUOTA_EXCEEDEDThis campaign's library is full. Delete a few files to make room.Failed queue row + storage meter warning
FOLDER_NAME_TAKENYou already have a folder with that name.Inline in the folder dialog
BUSINESS_UNVERIFIEDVerify your business before you launch a campaign.Banner on the wizard review step
VERIFICATION_IN_REVIEWYour business details are still under review. We'll email you when it's done.Pending badge on the verification card
NO_FUNDING_METHODAdd a funding method before you top up your wallet.Top-up modal CTA
CARD_DECLINEDYour bank declined that card. Try another one or use ACH.Inline under the card number
TEAM_MEMBER_EXISTSThat email is already on your team.Inline under the invite email
LAST_OWNEREvery brand account needs an owner. Make someone else an owner first.Toast + disabled role menu item
FORBIDDENYour role doesn't give you access to that.Full-page block on a gated route
DRAFT_NOT_FOUNDThat draft no longer exists.Toast + drafts list refresh
ITEM_NOT_FOUNDThat clip is no longer in the review queue.Toast + review queue refresh
ALREADY_DECIDEDSomeone already reviewed this clip.Toast + review queue refresh
NOT_APPEALABLEThis decision cannot be appealed.Inline, on the denied clip
APPEAL_WINDOW_CLOSEDThe time to appeal this has passed.Inline, on the denied clip
APPEAL_EXISTSYou have already appealed this decision.Inline + link to the open appeal
APPEAL_LIMITYou have five appeals open. Once one is decided you can file another: nothing will expire while you wait.Inline, with the reassurance that nothing lapses
APPEAL_NOT_FOUNDThat appeal no longer exists.Toast + back to the appeal list
SAME_REVIEWERYou made the original decision, so someone else has to settle the appeal.Toast + appeal stays open
NOT_LINKEDSign in to loopd.live with Discord first, then try this again.Discord bot: reply pointing at loopd.live sign-in
VALIDATIONPlease check the highlighted fields and try again.Inline field errors
NETWORKNetwork error. Check your connection and try again.Toast + retry
UNKNOWNSomething went wrong. Please try again.Toast
Foundations

Motion, icons and the mobile checklist

Motion is used to soften arrivals, never to entertain. Everything is 150–250ms, opacity plus a small y translation, and it happens once. Icons are lucide outline glyphs at 1.5px stroke; the only filled marks in the product are the platform logos and the black verified seal.

Motion

motion/react: subtle, once, under 250ms

Card enter with a 40ms stagger: the pattern every list uses.

Card 1

delay 0ms

Card 2

delay 40ms

Card 3

delay 80ms

Page / card enter
opacity 0→1, y 12→0, 220ms easeOut
Route content and card grids. Once only: never on re-render.
Stagger
40ms per child, max 6 children
Campaign grids and list rows. Beyond six it reads as slow.
Press
active:scale-[0.98]
Built into Button. The only transform a user triggers directly.
Sheet / dialog
vaul + Radix defaults, ~200ms
Never customized: the drag handle must feel native.
Accordion
200ms ease-out height
--animate-accordion-down/up in globals.css.
Skeleton
animate-pulse
The only looping animation in the product.
Reduced motion
Respect prefers-reduced-motion
Fades stay, translations drop. motion/react honors it by default.

Iconography

lucide-react at 16–20px, currentColor, 1.5px stroke

house
circle-plus
bell
settings
info
circle-check
trending-up
triangle-alert
lightbulb
timer
arrow-up-right
pencil
map-pin
refresh-cw
download
medal
lock-open
log-out
Platform glyphs
Instagram
TikTok
YouTube Shorts
X

Solid black, ~16–18px, from @/components/icons/platforms. They are brand marks: never recolor them, never wrap them in a colored well.

Mobile-feel checklist

Every screen ships against this list

  • Renders correctly at 390px with no horizontal scroll.
  • Primary CTA is full-width and pinned at the bottom of the card or sheet.
  • Fixed bottom bars use the safe-bottom utility; scrollable lists add pb-28.
  • Confirmations stay centered. Content flows use a drawer below 768px and a dialog above it.
  • Tap targets are at least 44px tall: button sizes lg and xl exist for this.
  • Two-column desktop layouts stack; live-preview panels move above the form or hide.
  • Tables become stacked rows or drop non-essential columns (Campaign is the first to go).
  • Text inputs are 16px+ on mobile so iOS does not zoom on focus.
  • No hover-only affordances: anything behind a hover has a visible mobile equivalent.
  • Long titles wrap rather than truncate; cards grow.