feat(theme): R10 S2 — re-close the rem system (px-frozen labels/chrome -> rem)

The ~124 px-frozen text-[Npx] labels didn't track the Settings font-scale slider
while their rem-sized neighbours did. Add text-2xs (0.6875rem) / text-3xs
(0.625rem) font-size-ONLY tokens (no paired line-height -> matches the arbitrary
they replace, zero vertical-rhythm shift) and codemod every text-[Npx] to a rem
class (rare 8/9/12/13px -> rem arbitraries).

Also: --hdr-h 68px -> 4.25rem so header content clears the rem-sized pills at
scale 1.3 instead of sliding under; NavSidebar/SidePanel slot widths + their
icon/badge px sizes go rem so the rails keep pace with their (now-scaling)
labels; a w-24/h-[54px] thumb -> h-13.5 to preserve 16:9 at scale. Media
thumbnails, page-width caps and fixed container widths deliberately stay px.

Verified via getComputedStyle at scale 1.06/1.3: text-2xs 11.66->14.3px, header
clearance 72->88px, nav slot 246->302px — all scale proportionally.
This commit is contained in:
2026-08-09 03:18:04 +02:00
parent 5e1abaaf8c
commit ab873bbcaa
37 changed files with 144 additions and 142 deletions
@@ -21,7 +21,7 @@ export default function ActiveFilterChips({
className="flex flex-wrap items-center gap-1.5 pb-3"
aria-label={t("feed.chips.label")}
>
<span className="text-[11px] uppercase tracking-wider text-muted mr-0.5">
<span className="text-2xs uppercase tracking-wider text-muted mr-0.5">
{t("feed.chips.label")}
</span>
{filters.map((f) => (
+4 -6
View File
@@ -61,9 +61,7 @@ function Badge({
? "border-warn/40 text-warn"
: "border-border text-muted";
return (
<span className={`shrink-0 text-[11px] px-2 py-0.5 rounded-full border ${cls}`}>
{children}
</span>
<span className={`shrink-0 text-2xs px-2 py-0.5 rounded-full border ${cls}`}>{children}</span>
);
}
@@ -180,7 +178,7 @@ function UsersRoles({ me }: { me: Me }) {
{u.display_name ?? u.email.split("@")[0]}
{self && <span className="text-muted"> · {t("users.roles.you")}</span>}
</div>
<div className="text-[11px] text-muted truncate">{u.email}</div>
<div className="text-2xs text-muted truncate">{u.email}</div>
</div>
{u.role === "admin" && <Badge tone="accent">{t("users.roles.admin")}</Badge>}
{u.is_demo && <Badge tone="muted">{t("users.roles.demo")}</Badge>}
@@ -436,7 +434,7 @@ function AdminDemo() {
<div className="min-w-0 flex-1">
<div className="text-sm truncate">{r.email}</div>
{r.added_by && (
<div className="text-[11px] text-muted truncate">
<div className="text-2xs text-muted truncate">
{t("settings.demo.addedBy", { who: r.added_by })}
</div>
)}
@@ -521,7 +519,7 @@ function InviteRow({
<div className="min-w-0 flex-1">
<div className="text-sm truncate">{inv.email}</div>
{inv.requested_at && (
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("settings.invites.requested", {
time: new Date(inv.requested_at).toLocaleString(),
})}
+1 -1
View File
@@ -84,7 +84,7 @@ export default function AuditLog() {
emptyText={t("audit.empty")}
/>
{truncated && (
<p className="text-[11px] text-muted mt-2">
<p className="text-2xs text-muted mt-2">
{t("audit.truncated", { returned: data!.returned, total: data!.total })}
</p>
)}
+1 -1
View File
@@ -71,7 +71,7 @@ export default function ChannelPicker({
random to anyone who hasn't set a priority. Only while unfiltered: a search reorders nothing
but does drop the rows that explain it. */}
{!q && all.length > 0 && (
<div className="px-2 pt-1.5 text-[11px] text-muted">{t("sidebar.channelOrder")}</div>
<div className="px-2 pt-1.5 text-2xs text-muted">{t("sidebar.channelOrder")}</div>
)}
<div
ref={fade.ref}
+3 -3
View File
@@ -23,7 +23,7 @@ interface TagChipProps {
function TagChip({ tag, onClick, title }: TagChipProps) {
const cls =
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full bg-accent text-accent-fg border border-accent max-w-36 truncate";
"shrink-0 text-3xs px-1.5 py-0.5 rounded-full bg-accent text-accent-fg border border-accent max-w-36 truncate";
return onClick ? (
<button
type="button"
@@ -91,7 +91,7 @@ function TagEditorMenu({
);
})}
{userTags.length === 0 && (
<p className="text-[11px] text-muted px-2 py-1.5">{t("channels.tags.noneYet")}</p>
<p className="text-2xs text-muted px-2 py-1.5">{t("channels.tags.noneYet")}</p>
)}
</div>
<div className="mt-1 pt-1.5 border-t border-border flex items-center gap-1">
@@ -238,7 +238,7 @@ export function ChannelTagsControl({
}, [hidden, overflowOpen]);
const pillClass =
"shrink-0 text-[10px] px-1.5 py-0.5 rounded-full bg-card text-muted border border-border";
"shrink-0 text-3xs px-1.5 py-0.5 rounded-full bg-card text-muted border border-border";
return (
// When `bounded`, a FIXED width keeps the cell's footprint constant under `table-layout:auto`: a
+2 -2
View File
@@ -748,7 +748,7 @@ export default function Channels() {
</span>
))}
</div>
<p className="text-[11px] text-muted mt-2">{t("channels.blocked.hint")}</p>
<p className="text-2xs text-muted mt-2">{t("channels.blocked.hint")}</p>
</>
)}
</div>
@@ -814,7 +814,7 @@ function SyncBadge({ ok, label, hint }: { ok: boolean; label: string; hint?: str
return (
<Tooltip hint={hint ?? ""}>
<span
className={`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full border ${
className={`inline-flex items-center gap-1 text-3xs px-1.5 py-0.5 rounded-full border ${
ok ? "border-accent/40 text-accent" : "border-border text-muted"
}`}
>
+1 -1
View File
@@ -183,7 +183,7 @@ export default function ChatThread({
}`}
>
{text}
<div className={`text-[10px] mt-1 ${mine ? "text-accent-fg/70" : "text-muted"}`}>
<div className={`text-3xs mt-1 ${mine ? "text-accent-fg/70" : "text-muted"}`}>
{relativeTime(m.created_at)}
</div>
</div>
+7 -9
View File
@@ -230,9 +230,7 @@ export default function ConfigPanel() {
{t("config.plexTest")}
</button>
</Tooltip>
{dirty && (
<p className="text-[11px] text-muted mt-2">{t("config.plexTestDirty")}</p>
)}
{dirty && <p className="text-2xs text-muted mt-2">{t("config.plexTestDirty")}</p>}
{plexResult && (
<div className="mt-3">
<p className="text-xs text-muted mb-2">
@@ -243,15 +241,15 @@ export default function ConfigPanel() {
</p>
{plexResult.media_check?.checked &&
(plexResult.media_check.ok ? (
<p className="text-[11px] text-success mb-2">{t("config.plexMediaOk")}</p>
<p className="text-2xs text-success mb-2">{t("config.plexMediaOk")}</p>
) : (
<p className="text-[11px] text-danger mb-2">
<p className="text-2xs text-danger mb-2">
{t("config.plexMediaMissing", {
path: plexResult.media_check.local_path ?? "",
})}
</p>
))}
<p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
<p className="text-2xs text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
<div className="flex flex-col gap-1.5">
{plexResult.sections.map((s) => (
<label
@@ -265,14 +263,14 @@ export default function ConfigPanel() {
className="accent-accent"
/>
<span>{s.title}</span>
<span className="text-[11px] text-muted">
<span className="text-2xs text-muted">
{s.type === "movie" ? t("config.plexMovies") : t("config.plexShows")}
{s.count != null ? ` · ${s.count}` : ""}
</span>
</label>
))}
</div>
<p className="text-[11px] text-muted mt-2">{t("config.plexPickHint")}</p>
<p className="text-2xs text-muted mt-2">{t("config.plexPickHint")}</p>
</div>
)}
</div>
@@ -348,7 +346,7 @@ function Field({
<p className="text-xs text-muted leading-relaxed mt-0.5">
{t(`config.fields.${item.key}.hint`, "")}
</p>
<p className="text-[11px] text-muted mt-1">{status}</p>
<p className="text-2xs text-muted mt-1">{status}</p>
</div>
<div className="flex flex-col items-end gap-1.5 shrink-0">
+2 -2
View File
@@ -227,7 +227,7 @@ function DownloadRow({
<div className="min-w-0 flex-1">
<div className="font-medium leading-snug line-clamp-1 flex items-center gap-1.5">
{job.job_kind === "edit" && (
<span className="inline-flex items-center gap-1 shrink-0 px-1.5 py-0.5 rounded-sm text-[10px] font-semibold uppercase tracking-wide bg-accent/15 text-accent">
<span className="inline-flex items-center gap-1 shrink-0 px-1.5 py-0.5 rounded-sm text-3xs font-semibold uppercase tracking-wide bg-accent/15 text-accent">
<Scissors className="w-3 h-3" /> {t("downloads.clipBadge")}
</span>
)}
@@ -280,7 +280,7 @@ function DownloadRow({
style={{ width: `${job.progress}%` }}
/>
</div>
<div className="text-[11px] text-muted mt-0.5 flex gap-2">
<div className="text-2xs text-muted mt-0.5 flex gap-2">
<span>{job.progress}%</span>
{job.speed_bps ? <span>{formatSpeed(job.speed_bps)}</span> : null}
{job.eta_s ? <span>{formatEta(job.eta_s)}</span> : null}
+7 -7
View File
@@ -288,7 +288,7 @@ export default function GlassTuner() {
>
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60">
<span className="text-sm font-semibold text-fg">◐ Glass Tuner</span>
<span className="text-[10px] uppercase tracking-wider text-muted">lab</span>
<span className="text-3xs uppercase tracking-wider text-muted">lab</span>
<button
onClick={() => persistOpen(false)}
className="ml-auto text-muted hover:text-fg text-lg leading-none px-1"
@@ -301,7 +301,7 @@ export default function GlassTuner() {
<div className="overflow-y-auto px-3 py-3 flex flex-col gap-4 text-sm">
{/* Treatment presets */}
<div className="flex flex-col gap-1.5">
<div className="text-[10px] uppercase tracking-wider text-muted font-semibold">
<div className="text-3xs uppercase tracking-wider text-muted font-semibold">
Dark treatment
</div>
<div className="grid grid-cols-2 gap-1.5">
@@ -313,7 +313,7 @@ export default function GlassTuner() {
className="glass-card glass-hover rounded-lg px-2 py-1.5 text-left text-xs text-fg"
>
<b className="block font-semibold">{p.name}</b>
<span className="text-muted text-[10px]">{p.hint}</span>
<span className="text-muted text-3xs">{p.hint}</span>
</button>
))}
</div>
@@ -351,7 +351,7 @@ export default function GlassTuner() {
{tab === "palette" && (
<div className="flex flex-col gap-2">
<p className="text-[10px] text-muted leading-relaxed">
<p className="text-3xs text-muted leading-relaxed">
Overrides the live scheme (inline on &lt;html&gt;). Reset clears these back to
the scheme.
</p>
@@ -364,7 +364,7 @@ export default function GlassTuner() {
className="w-8 h-6 rounded-sm border border-border bg-transparent cursor-pointer"
/>
<span className="flex-1">{p.label}</span>
<code className="text-[10px] text-muted">{swatches[p.k]}</code>
<code className="text-3xs text-muted">{swatches[p.k]}</code>
</label>
))}
</div>
@@ -372,7 +372,7 @@ export default function GlassTuner() {
{/* Export */}
<div className="flex flex-col gap-1.5 pt-1 border-t border-border/50">
<div className="text-[10px] uppercase tracking-wider text-muted font-semibold">
<div className="text-3xs uppercase tracking-wider text-muted font-semibold">
Export → bake in
</div>
<button
@@ -419,7 +419,7 @@ function Ctl({ s, value, onChange }: { s: Slider; value: number; onChange: (v: n
step={s.step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="w-16 text-right text-[11px] tabular-nums rounded-sm bg-card border border-border px-1.5 py-0.5 text-accent"
className="w-16 text-right text-2xs tabular-nums rounded-sm bg-card border border-border px-1.5 py-0.5 text-accent"
/>
<input
type="range"
+1 -1
View File
@@ -56,7 +56,7 @@ export default function LanguageSwitcher({
{rail ? (
/* Current-language badge (HU/EN) so the active language reads at a glance without
opening the menu. The ring matches the page background to detach it from the icon. */
<span className="absolute -bottom-1 -right-1 text-[8px] font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
<span className="absolute -bottom-1 -right-1 text-[0.5rem] font-bold leading-none px-1 py-px rounded-sm bg-accent text-accent-fg ring-2 ring-bg">
{current.code.toUpperCase()}
</span>
) : (
+2 -2
View File
@@ -165,7 +165,7 @@ function ConversationList({
>
{c.partner.name}
</span>
<span className="ml-auto text-[11px] text-muted shrink-0">
<span className="ml-auto text-2xs text-muted shrink-0">
{relativeTime(c.last_message.created_at)}
</span>
</div>
@@ -176,7 +176,7 @@ function ConversationList({
{previewText(c)}
</span>
{c.unread > 0 && (
<span className="ml-auto shrink-0 min-w-5 h-5 px-1.5 grid place-items-center rounded-full bg-accent text-accent-fg text-[11px] font-semibold">
<span className="ml-auto shrink-0 min-w-5 h-5 px-1.5 grid place-items-center rounded-full bg-accent text-accent-fg text-2xs font-semibold">
{c.unread}
</span>
)}
+12 -12
View File
@@ -198,12 +198,12 @@ export default function NavSidebar({
}`}
>
<span className="relative shrink-0">
<Icon className="w-[18px] h-[18px]" />
<Icon className="w-4.5 h-4.5" />
{/* Collapsed rail: a numeric badge centred on a circle at the icon corner; the ring
matches the row background so it reads cleanly whether the row is active or not. */}
{show && slim && (
<span
className={`absolute -top-2 -right-2 min-w-[16px] h-4 px-1 rounded-full text-[10px] font-bold leading-none grid place-items-center ring-2 ${badgeColor} ${
className={`absolute -top-2 -right-2 min-w-4 h-4 px-1 rounded-full text-3xs font-bold leading-none grid place-items-center ring-2 ${badgeColor} ${
active ? "ring-accent" : "ring-bg"
}`}
>
@@ -214,7 +214,7 @@ export default function NavSidebar({
{!slim && <span className="truncate">{label}</span>}
{show && !slim && (
<span
className={`ml-auto min-w-[18px] h-[18px] px-1.5 rounded-full text-[11px] font-semibold leading-none inline-flex items-center justify-center tabular-nums ${badgeColor}`}
className={`ml-auto min-w-4.5 h-4.5 px-1.5 rounded-full text-2xs font-semibold leading-none inline-flex items-center justify-center tabular-nums ${badgeColor}`}
>
{badge > 99 ? "99+" : badge}
</span>
@@ -226,14 +226,14 @@ export default function NavSidebar({
return (
// Layout slot: reserves only the slim footprint when unpinned, so the expanded rail (below)
// overlays content instead of pushing it. Pinned reserves the full width (in-flow).
<div className={`relative shrink-0 transition-[width] ${pinned ? "w-[232px]" : "w-20"}`}>
<div className={`relative shrink-0 transition-[width] ${pinned ? "w-58" : "w-20"}`}>
<nav
// Peek handlers sit on the CARD, not the slot: the slot is wider than the card by its
// margin, and a hover zone out there would fire from the dead space next to the rail —
// leaving the side panel to the left would pop the rail open with nothing under the cursor.
{...peek.handlers}
className={`glass absolute top-0 bottom-0 left-0 z-rail rounded-2xl flex flex-col py-3 m-3 transition-[width] ${
slim ? "w-[56px] px-2" : "w-52 px-3"
slim ? "w-14 px-2" : "w-52 px-3"
}`}
aria-label={t("nav.primary")}
>
@@ -270,7 +270,7 @@ export default function NavSidebar({
<div className="my-2 mx-auto w-7 border-t-2 border-border" />
) : (
<div className="mt-4 mb-1 px-2.5 flex items-center gap-2">
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted/70">
<span className="text-3xs font-semibold uppercase tracking-wider text-muted/70">
{t("nav.adminSection")}
</span>
<span className="flex-1 border-t border-border" />
@@ -314,7 +314,7 @@ export default function NavSidebar({
: "text-muted hover:text-fg hover:bg-card"
}`}
>
<Settings className="w-[18px] h-[18px] shrink-0" />
<Settings className="w-4.5 h-4.5 shrink-0" />
{!slim && <span className="truncate">{t("header.account.settings")}</span>}
</button>
@@ -329,7 +329,7 @@ export default function NavSidebar({
<AvatarImg
src={me.avatar_url}
fallback={name}
className="w-[22px] h-[22px] rounded-full text-[10px]"
className="w-5.5 h-5.5 rounded-full text-3xs"
/>
</span>
{!slim && (
@@ -356,7 +356,7 @@ export default function NavSidebar({
</div>
</div>
{me.role === "admin" && (
<div className="flex items-center gap-1.5 mt-2 text-[11px] font-medium text-accent">
<div className="flex items-center gap-1.5 mt-2 text-2xs font-medium text-accent">
<Shield className="w-3.5 h-3.5" />
{t("header.account.admin")}
</div>
@@ -373,13 +373,13 @@ export default function NavSidebar({
<AvatarImg
src={a.avatar_url}
fallback={a.display_name ?? a.email}
className="w-6 h-6 rounded-full text-[10px] shrink-0"
className="w-6 h-6 rounded-full text-3xs shrink-0"
/>
<span className="min-w-0 text-left">
<span className="block text-[13px] text-fg truncate">
<span className="block text-[0.8125rem] text-fg truncate">
{a.display_name ?? a.email.split("@")[0]}
</span>
<span className="block text-[11px] text-muted truncate">{a.email}</span>
<span className="block text-2xs text-muted truncate">{a.email}</span>
</span>
</button>
))}
@@ -263,7 +263,7 @@ function NotificationRow({
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2">
<span className="font-medium text-sm">{title}</span>
<span className="text-[11px] text-muted shrink-0">{relativeTime(n.created_at)}</span>
<span className="text-2xs text-muted shrink-0">{relativeTime(n.created_at)}</span>
</div>
{body && <div className="text-sm text-muted mt-0.5">{body}</div>}
{videos.length > 0 && (
@@ -348,15 +348,15 @@ function ClientActivityRow({
<div className="text-sm wrap-break-word">
{n.message}
{n.repeat > 1 && (
<span className="ml-1.5 align-middle text-[11px] font-semibold text-muted tabular-nums">
<span className="ml-1.5 align-middle text-2xs font-semibold text-muted tabular-nums">
×{n.repeat}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[11px] text-muted">{relativeFromMs(n.ts)}</span>
<span className="text-2xs text-muted">{relativeFromMs(n.ts)}</span>
{needsAction && (
<span className="text-[10px] uppercase tracking-wide font-semibold text-accent">
<span className="text-3xs uppercase tracking-wide font-semibold text-accent">
{t("notifications.actionNeeded")}
</span>
)}
+2 -2
View File
@@ -1056,7 +1056,7 @@ export default function PlayerModal({
)}
{/* Discreet badge while YouTube's own controls have taken over (overlay yielded). */}
{playerError == null && nativeControls && (
<div className="pointer-events-none absolute top-3 left-3 z-menu flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-white/90 shadow-lg backdrop-blur-xs">
<div className="pointer-events-none absolute top-3 left-3 z-menu flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-2xs text-white/90 shadow-lg backdrop-blur-xs">
<Settings className="h-3.5 w-3.5" />
{t("player.nativeControls")}
</div>
@@ -1250,7 +1250,7 @@ export default function PlayerModal({
<span>· {formatDuration(active.duration_seconds)}</span>
)}
{active.live_status === "was_live" && (
<span className="bg-accent text-accent-fg text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
<span className="bg-accent text-accent-fg text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
{t("player.stream")}
</span>
)}
+3 -3
View File
@@ -573,7 +573,7 @@ export default function Playlists() {
<PageToolbar>
<div className="px-4 pt-3 pb-2">
<div className="flex gap-3 items-start mb-4">
<div className="w-24 h-[54px] rounded-lg bg-card overflow-hidden shrink-0">
<div className="w-24 h-13.5 rounded-lg bg-card overflow-hidden shrink-0">
{items[0]?.thumbnail_url && (
<img src={items[0].thumbnail_url} alt="" className="w-full h-full object-cover" />
)}
@@ -681,7 +681,7 @@ export default function Playlists() {
{linked && detail.dirty && (
<span
title={t("playlists.unsyncedHint")}
className="inline-flex items-center gap-1.5 text-[11px] text-accent ml-1"
className="inline-flex items-center gap-1.5 text-2xs text-accent ml-1"
>
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
{t("playlists.unsynced")}
@@ -724,7 +724,7 @@ export default function Playlists() {
stored order ALREADY matches the sort produced a warning, no Save button and no
visible change, which reads as a malfunction. Now the line says which of the three
situations you are in; the disabled grip still explains itself on hover/focus. */}
{viewActive && <span className="text-[11px] text-muted/80">{viewStatus}</span>}
{viewActive && <span className="text-2xs text-muted/80">{viewStatus}</span>}
{canEdit && (
<div className="ml-auto">
<UndoToolbar
+3 -3
View File
@@ -154,7 +154,7 @@ export default function PlaylistsRail({
value={plSort.key}
onChange={(e) => setPlSort({ ...plSort, key: e.target.value as PlSort["key"] })}
aria-label={t("playlists.sortLabel")}
className="flex-1 min-w-0 bg-card border border-border rounded-md px-1.5 py-1 text-[11px] outline-hidden focus:border-accent"
className="flex-1 min-w-0 bg-card border border-border rounded-md px-1.5 py-1 text-2xs outline-hidden focus:border-accent"
>
<option value="custom">{t("playlists.railSortCustom")}</option>
<option value="name">{t("playlists.railSortName")}</option>
@@ -246,7 +246,7 @@ export default function PlaylistsRail({
)}
</div>
<div className="min-w-0">
<div className="flex items-center gap-1 text-[13px] text-fg">
<div className="flex items-center gap-1 text-[0.8125rem] text-fg">
<span className="truncate">{plName(pl)}</span>
{(pl.source === "youtube" || pl.yt_playlist_id) && (
<Youtube
@@ -254,7 +254,7 @@ export default function PlaylistsRail({
/>
)}
</div>
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("playlists.itemCount", { count: pl.item_count })}
{pl.total_duration_seconds > 0 &&
` · ${formatDuration(pl.total_duration_seconds)}`}
+13 -13
View File
@@ -342,7 +342,7 @@ export default function PlexBrowse() {
)}
<div className="min-w-0">
<div className="text-sm font-medium truncate max-w-[150px]">{p.name}</div>
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("plex.people.count", { count: p.count })}
</div>
</div>
@@ -525,18 +525,18 @@ function PlexPosterCard({
)}
{/* Watched badge when not hovering (the toggle replaces it on hover). */}
{card.status === "watched" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.watched")}
</span>
)}
{card.type === "show" && card.season_count != null && (
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
<span className="absolute bottom-1.5 left-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.seasons", { count: card.season_count })}
</span>
)}
{/* Aggregate in-progress badge for a partially-watched show. */}
{card.type === "show" && card.status === "in_progress" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.inProgress")}
</span>
)}
@@ -679,7 +679,7 @@ function ActionBtn({
<Icon className="w-4 h-4 shrink-0" />
<span className="flex flex-col items-start leading-tight">
<span>{label}</span>
{sub && <span className="text-[11px] font-normal opacity-70">{sub}</span>}
{sub && <span className="text-2xs font-normal opacity-70">{sub}</span>}
</span>
</button>
);
@@ -753,16 +753,16 @@ function SeasonCard({
</button>
)}
{watched && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.watched")}
</span>
)}
{se.status === "in_progress" && (
<span className="pointer-events-none absolute top-1.5 right-1.5 text-[10px] bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
<span className="pointer-events-none absolute top-1.5 right-1.5 text-3xs bg-black/75 text-accent px-1.5 py-0.5 rounded-sm group-hover:opacity-0">
{t("plex.inProgress")}
</span>
)}
<span className="absolute bottom-1.5 left-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
<span className="absolute bottom-1.5 left-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.series.episodeCount", { count: se.episode_count })}
</span>
</div>
@@ -811,7 +811,7 @@ function CastStrip({
>
{c.name}
</div>
{c.role && <div className="text-[11px] text-muted line-clamp-1">{c.role}</div>}
{c.role && <div className="text-2xs text-muted line-clamp-1">{c.role}</div>}
</>
);
return onFilter ? (
@@ -857,7 +857,7 @@ function RelatedStrip({ related, onOpen }: { related: PlexCard[]; onOpen: (id: s
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
/>
{r.status === "watched" && (
<span className="absolute top-1.5 right-1.5 text-[10px] bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
<span className="absolute top-1.5 right-1.5 text-3xs bg-black/70 text-white px-1.5 py-0.5 rounded-sm">
{t("plex.watched")}
</span>
)}
@@ -948,7 +948,7 @@ function EpisodeCard({
>
<div className="min-w-0 flex-1">
{withShowTitle && ep.show_title && (
<div className="text-[11px] text-muted truncate">
<div className="text-2xs text-muted truncate">
{ep.show_title}
{ep.season_number != null && ep.episode_number != null
? ` · S${ep.season_number}·E${ep.episode_number}`
@@ -959,7 +959,7 @@ function EpisodeCard({
{!withShowTitle && <span className="text-muted mr-1">{ep.episode_number}.</span>}
{ep.title}
</div>
<div className="text-[11px] text-muted mt-0.5">{formatRuntime(ep.duration_seconds)}</div>
<div className="text-2xs text-muted mt-0.5">{formatRuntime(ep.duration_seconds)}</div>
</div>
{onAdd && (
<button
@@ -1113,7 +1113,7 @@ function PlexShowView({
{show.genres.map((g) => (
<Filterable
key={g}
className="text-[11px] rounded-full bg-surface px-2 py-0.5 text-muted"
className="text-2xs rounded-full bg-surface px-2 py-0.5 text-muted"
onClick={onFilter ? () => onFilter({ genres: [g] }) : undefined}
>
{g}
+3 -3
View File
@@ -360,7 +360,7 @@ export default function PlexInfo({
</div>
{c.role && (
<div
className={`truncate text-[11px] ${overlay ? "text-white/55" : "text-muted"}`}
className={`truncate text-2xs ${overlay ? "text-white/55" : "text-muted"}`}
>
{c.role}
</div>
@@ -420,7 +420,7 @@ export default function PlexInfo({
className="h-full w-full object-cover transition group-hover/col:scale-105"
/>
{m.status === "watched" && (
<span className="absolute right-1 top-1 rounded-sm bg-black/70 px-1 text-[9px] text-white">
<span className="absolute right-1 top-1 rounded-sm bg-black/70 px-1 text-[0.5625rem] text-white">
✓
</span>
)}
@@ -447,7 +447,7 @@ export default function PlexInfo({
>
{m.title}
</div>
{m.year ? <div className="text-[11px] text-muted">{m.year}</div> : null}
{m.year ? <div className="text-2xs text-muted">{m.year}</div> : null}
</button>
))}
</div>
+13 -15
View File
@@ -1221,7 +1221,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
{/* Hover timestamp tooltip — follows the cursor along the bar. */}
{hover && duration > 0 && (
<div
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-[11px] tabular-nums text-white"
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-2xs tabular-nums text-white"
style={{ left: `${hover.frac * 100}%` }}
>
{fmt(hover.t)}
@@ -1317,7 +1317,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
{/* hover value tooltip (0–100), like the seek bar */}
{volHover != null && (
<div
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-[11px] tabular-nums text-white"
className="pointer-events-none absolute bottom-full mb-2 -translate-x-1/2 rounded-sm bg-black/90 px-1.5 py-0.5 text-2xs tabular-nums text-white"
style={{ left: `${volHover * 100}%` }}
>
{Math.round(volHover * 100)}
@@ -1431,7 +1431,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
<button
key={tab.key}
onClick={() => setSettingsTab(tab.key)}
className={`flex-1 whitespace-nowrap rounded px-2 py-1 text-[11px] ${
className={`flex-1 whitespace-nowrap rounded px-2 py-1 text-2xs ${
activeTab === tab.key
? "bg-white/15 text-accent"
: "text-white/70 hover:bg-white/10"
@@ -1537,7 +1537,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
onChange={(v) => patchPrefs({ subPos: v })}
/>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.subColor")}
</span>
<input
@@ -1548,11 +1548,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
/>
</div>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.subBg")}
</span>
<div className="flex items-center gap-2">
<label className="flex items-center gap-1 text-[11px] text-white/70">
<label className="flex items-center gap-1 text-2xs text-white/70">
<input
type="checkbox"
checked={prefs.subBg === "transparent"}
@@ -1586,7 +1586,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
/>
{prefs.subShadow > 0 && (
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.subShadowColor")}
</span>
<input
@@ -1622,7 +1622,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
onChange={(v) => patchPrefs({ clockSize: v })}
/>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">
<span className="text-[0.75rem] text-white/80">
{t("plex.player.clockColor")}
</span>
<input
@@ -1643,7 +1643,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
<button
key={st}
onClick={() => patchPrefs({ clockDateStyle: st })}
className={`flex-1 rounded px-2 py-1 text-[11px] ${
className={`flex-1 rounded px-2 py-1 text-2xs ${
prefs.clockDateStyle === st
? "bg-white/15 text-accent"
: "text-white/70 hover:bg-white/10"
@@ -1776,9 +1776,7 @@ function BackClose({ onBack }: { onBack: () => void }) {
// --- settings-panel building blocks ------------------------------------------------------------
function PanelHead({ children }: { children: ReactNode }) {
return (
<div className="px-1 pt-2 pb-1 text-[11px] uppercase tracking-wide text-white/50">
{children}
</div>
<div className="px-1 pt-2 pb-1 text-2xs uppercase tracking-wide text-white/50">{children}</div>
);
}
function Slider({
@@ -1802,7 +1800,7 @@ function Slider({
}) {
return (
<div className="px-1 py-1">
<div className="mb-1 flex items-center justify-between text-[12px] text-white/80">
<div className="mb-1 flex items-center justify-between text-[0.75rem] text-white/80">
<span>{label}</span>
<span className="flex items-center gap-1.5">
<span className="tabular-nums text-white/60">
@@ -1839,7 +1837,7 @@ function Toggle({
}) {
return (
<label className="flex cursor-pointer items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">{label}</span>
<span className="text-[0.75rem] text-white/80">{label}</span>
<input
type="checkbox"
checked={checked}
@@ -1879,7 +1877,7 @@ function Ctrl({
{children}
</button>
<span
className={`pointer-events-none absolute bottom-full ${pos} mb-2 whitespace-nowrap rounded-sm bg-black/90 px-2 py-1 text-[11px] text-white opacity-0 transition group-hover/ctrl:opacity-100`}
className={`pointer-events-none absolute bottom-full ${pos} mb-2 whitespace-nowrap rounded-sm bg-black/90 px-2 py-1 text-2xs text-white opacity-0 transition group-hover/ctrl:opacity-100`}
>
{label}
</span>
+5 -5
View File
@@ -153,7 +153,7 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
className={`flex items-center gap-2.5 rounded-lg ${dense ? "px-1.5 py-1" : "glass-card p-1.5"}`}
>
<DragHandle {...attributes} {...listeners} />
<span className="w-5 shrink-0 text-center text-[11px] text-muted tabular-nums">
<span className="w-5 shrink-0 text-center text-2xs text-muted tabular-nums">
{ep.episode_number}
</span>
<button
@@ -165,7 +165,7 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
>
<img src={ep.thumb} alt="" loading="lazy" className="h-full w-full object-cover" />
{ep.status === "watched" && (
<span className="absolute right-0.5 top-0.5 rounded-sm bg-black/70 px-1 text-[9px] text-white">
<span className="absolute right-0.5 top-0.5 rounded-sm bg-black/70 px-1 text-[0.5625rem] text-white">
✓
</span>
)}
@@ -174,7 +174,7 @@ function EpisodeRow({ ep, ctx, dense }: { ep: PlexCard; ctx: Ctx; dense: boolean
<Play className="h-4 w-4 text-white" fill="currentColor" />
</div>
</div>
<div className="min-w-0 truncate text-[13px]">{ep.title}</div>
<div className="min-w-0 truncate text-[0.8125rem]">{ep.title}</div>
</button>
<button
onClick={() => ctx.onRemove([ep.id])}
@@ -277,7 +277,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
)}
<button onClick={() => ctx.toggleShow(block.showKey)} className="min-w-0 flex-1 text-left">
<div className="truncate text-sm font-semibold">{block.title}</div>
<div className="text-[11px] text-muted">
<div className="text-2xs text-muted">
{t("plex.playlist.episodes", { count: block.items.length })}
</div>
</button>
@@ -316,7 +316,7 @@ function ShowGroup({ block, ctx }: { block: Extract<Block, { kind: "show" }>; ct
)}
</button>
)}
<span className="text-[11px] font-medium uppercase tracking-wide text-muted">
<span className="text-2xs font-medium uppercase tracking-wide text-muted">
{seasonLabel(s)} · {s.items.length}
</span>
<button
+2 -2
View File
@@ -46,11 +46,11 @@ function MatchToggle({
const { t } = useTranslation();
return (
<div className="mb-1.5 flex items-center justify-between">
<span className="text-[10px] uppercase tracking-wide text-muted">
<span className="text-3xs uppercase tracking-wide text-muted">
{t("plex.filter.matchLabel")}
</span>
<div
className="flex items-center rounded-full border border-border bg-card p-0.5 text-[10px]"
className="flex items-center rounded-full border border-border bg-card p-0.5 text-3xs"
role="group"
aria-label={t("plex.filter.matchLabel")}
>
+1 -1
View File
@@ -38,7 +38,7 @@ export default function ReleaseNotes({
<h3 className="text-base font-semibold">v{r.version}</h3>
<span className="text-xs text-muted">{fmtDate(r.date)}</span>
{r.sha && (
<span className="text-[11px] font-mono text-muted bg-surface px-1.5 py-0.5 rounded-sm">
<span className="text-2xs font-mono text-muted bg-surface px-1.5 py-0.5 rounded-sm">
{r.sha}
</span>
)}
+1 -1
View File
@@ -135,7 +135,7 @@ export default function SavedViewsWidget({
return (
<div className="space-y-2">
{views.length === 0 && !naming && (
<p className="text-[11px] text-muted leading-snug">{t("views.empty")}</p>
<p className="text-2xs text-muted leading-snug">{t("views.empty")}</p>
)}
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
+8 -8
View File
@@ -75,7 +75,7 @@ function StatusDot({
function StatusLegend() {
const { t } = useTranslation();
return (
<div className="flex items-center gap-x-3 gap-y-1 flex-wrap text-[11px] text-muted mb-2">
<div className="flex items-center gap-x-3 gap-y-1 flex-wrap text-2xs text-muted mb-2">
{(["running", "ok", "idle", "error", "skipped"] as const).map((k) => (
<span key={k} className="inline-flex items-center gap-1.5">
<StatusDot k={k} withTooltip={false} />
@@ -97,7 +97,7 @@ function JobProgress({ p }: { p: SchedulerJob["progress"] }) {
p?.total && p.total > 0 ? Math.min(100, Math.round((p.current / p.total) * 100)) : null;
return (
<div className="mt-1.5">
<div className="flex items-center justify-between text-[11px] text-muted mb-0.5">
<div className="flex items-center justify-between text-2xs text-muted mb-0.5">
<span className="truncate">{phase}</span>
<span className="tabular-nums shrink-0">
{p?.total != null
@@ -203,7 +203,7 @@ function JobRow({
</Tooltip>
)}
</div>
<div className="text-[11px] text-muted truncate">
<div className="text-2xs text-muted truncate">
{job.running
? t("scheduler.runningNow")
: job.last_finished
@@ -217,12 +217,12 @@ function JobRow({
</div>
{job.running && <JobProgress p={job.progress} />}
</div>
<div className="shrink-0 text-right text-[11px] text-muted tabular-nums">
<div className="shrink-0 text-right text-2xs text-muted tabular-nums">
{job.running ? (
<span className="text-accent">{t("scheduler.active")}</span>
) : next != null ? (
<>
<div className="uppercase tracking-wide text-[10px]">{t("scheduler.nextRun")}</div>
<div className="uppercase tracking-wide text-3xs">{t("scheduler.nextRun")}</div>
<div>{fmtCountdown(next)}</div>
</>
) : (
@@ -291,7 +291,7 @@ function MaintenanceSettings({
{t("common.save")}
</button>
</div>
<div className="text-[11px] text-muted mt-1.5">
<div className="text-2xs text-muted mt-1.5">
{t("scheduler.maintenance.batchNote", {
default: m.revalidate_batch_default.toLocaleString(),
})}
@@ -330,7 +330,7 @@ function Stat({
</Tooltip>
</div>
<div className="text-xl font-semibold mt-1 tabular-nums">{value}</div>
{sub && <div className="text-[11px] text-muted">{sub}</div>}
{sub && <div className="text-2xs text-muted">{sub}</div>}
</div>
);
}
@@ -595,7 +595,7 @@ export default function Scheduler() {
style={{ width: `${quotaPct}%` }}
/>
</div>
<div className="text-[11px] text-muted mt-1.5">{t("scheduler.quotaNote")}</div>
<div className="text-2xs text-muted mt-1.5">{t("scheduler.quotaNote")}</div>
</div>
</div>
+2 -2
View File
@@ -323,7 +323,7 @@ function AccessRow({
</p>
</div>
{granted ? (
<span className="shrink-0 text-[11px] px-2 py-1 rounded-full border border-accent/40 text-accent">
<span className="shrink-0 text-2xs px-2 py-1 rounded-full border border-accent/40 text-accent">
{t("settings.account.granted")}
</span>
) : (
@@ -422,7 +422,7 @@ function SignInMethods({ me }: { me: Me }) {
</p>
</div>
{me.has_google ? (
<span className="shrink-0 text-[11px] px-2 py-1 rounded-full border border-accent/40 text-accent">
<span className="shrink-0 text-2xs px-2 py-1 rounded-full border border-accent/40 text-accent">
{t("settings.account.googleLink.connected")}
</span>
) : (
+5 -7
View File
@@ -47,9 +47,7 @@ export default function SidePanel({
// content instead of pushing it. Pinned reserves the full width (in-flow), as before.
<div
{...peek.handlers}
className={`hidden md:block relative shrink-0 transition-[width] ${
pinned ? "w-[268px]" : "w-9"
}`}
className={`hidden md:block relative shrink-0 transition-[width] ${pinned ? "w-67" : "w-9"}`}
>
{/* The tab: a small handle flush against the nav rail, flat on the docked edge. It stays
mounted while the card is open — swapping it out from under the pointer would drop the very
@@ -66,12 +64,12 @@ export default function SidePanel({
>
<span className="text-accent">{icon}</span>
{count > 0 && (
<span className="bg-accent text-accent-fg text-[10px] font-bold px-1.5 py-0.5 rounded-full tabular-nums">
<span className="bg-accent text-accent-fg text-3xs font-bold px-1.5 py-0.5 rounded-full tabular-nums">
{count > 9 ? "9+" : count}
</span>
)}
<span
className="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted"
className="text-2xs font-semibold uppercase tracking-[0.18em] text-muted"
style={{ writingMode: "vertical-rl", transform: "rotate(180deg)" }}
>
{title}
@@ -98,7 +96,7 @@ export default function SidePanel({
{count > 0 && (
<span
title={t("sidebar.activeCount", { count })}
className="shrink-0 min-w-[18px] h-[18px] px-1.5 rounded-full bg-accent/15 text-accent text-[11px] font-semibold inline-flex items-center justify-center tabular-nums"
className="shrink-0 min-w-4.5 h-4.5 px-1.5 rounded-full bg-accent/15 text-accent text-2xs font-semibold inline-flex items-center justify-center tabular-nums"
>
{count}
</span>
@@ -138,7 +136,7 @@ export default function SidePanel({
className="flex-1 min-h-0 overflow-y-auto no-scrollbar"
>
<div className="p-3 space-y-3">
{editing && editHint && <div className="text-[11px] text-muted">{editHint}</div>}
{editing && editHint && <div className="text-2xs text-muted">{editHint}</div>}
{children}
</div>
</div>
+7 -7
View File
@@ -54,7 +54,7 @@ function TagChip({
>
<span>{tag.name}</span>
<span
className={`tabular-nums text-[10px] leading-none px-1 py-0.5 rounded-full ${
className={`tabular-nums text-3xs leading-none px-1 py-0.5 rounded-full ${
active ? "bg-accent-fg/20 text-accent-fg" : "bg-muted/15 text-muted"
}`}
>
@@ -232,7 +232,7 @@ export default function Sidebar({
onClick={() =>
setFilters({ ...filters, dateFrom: undefined, dateTo: undefined })
}
className="text-[11px] text-muted hover:text-accent self-end"
className="text-2xs text-muted hover:text-accent self-end"
>
{t("sidebar.clearDates")}
</button>
@@ -255,7 +255,7 @@ export default function Sidebar({
/>
))}
{facetsReady && chips.length === 0 && (
<span className="text-[11px] text-muted">{t("sidebar.noMatchingTags")}</span>
<span className="text-2xs text-muted">{t("sidebar.noMatchingTags")}</span>
)}
</div>
);
@@ -268,11 +268,11 @@ export default function Sidebar({
className="flex items-center justify-between mb-1.5"
title={t("sidebar.tagModeTooltip")}
>
<span className="text-[10px] uppercase tracking-wide text-muted">
<span className="text-3xs uppercase tracking-wide text-muted">
{t("sidebar.match")}
</span>
<div
className="flex items-center rounded-full border border-border bg-card p-0.5 text-[10px]"
className="flex items-center rounded-full border border-border bg-card p-0.5 text-3xs"
role="group"
aria-label={t("sidebar.match")}
>
@@ -303,7 +303,7 @@ export default function Sidebar({
/>
))}
{facetsReady && chips.length === 0 && (
<span className="text-[11px] text-muted">{t("sidebar.noMatchingTags")}</span>
<span className="text-2xs text-muted">{t("sidebar.noMatchingTags")}</span>
)}
</div>
</>
@@ -323,7 +323,7 @@ export default function Sidebar({
/>
))}
{facetsReady && tagChips.length === 0 && (
<span className="text-[11px] text-muted">{t("sidebar.noMatchingTags")}</span>
<span className="text-2xs text-muted">{t("sidebar.noMatchingTags")}</span>
)}
<button
onClick={() => setTagManagerOpen(true)}
+1 -1
View File
@@ -40,7 +40,7 @@ export default function Tabs({
{tb.label}
{tb.badge != null && tb.badge > 0 && (
<span
className={`text-[11px] leading-none px-1.5 py-0.5 rounded-full ${
className={`text-2xs leading-none px-1.5 py-0.5 rounded-full ${
on ? "bg-accent-fg/20" : "bg-accent/20 text-accent"
}`}
>
+1 -1
View File
@@ -76,7 +76,7 @@ function TagRow({
popover={pop}
className="glass-menu w-56 rounded-xl p-1.5 animate-[popIn_0.16s_ease]"
>
<div className="text-[11px] uppercase tracking-wide text-muted px-2 py-1">
<div className="text-2xs uppercase tracking-wide text-muted px-2 py-1">
{t("tagManager.onChannels")}
</div>
{/* Inner scroller so the scroll-fade mask rides the scrolling box, not the glass panel. */}
+1 -1
View File
@@ -41,7 +41,7 @@ export default function Toaster() {
<div className="text-sm wrap-break-word">
{toast.message}
{toast.repeat > 1 && (
<span className="ml-1.5 align-middle text-[11px] font-semibold text-muted tabular-nums">
<span className="ml-1.5 align-middle text-2xs font-semibold text-muted tabular-nums">
×{toast.repeat}
</span>
)}
+3 -3
View File
@@ -245,12 +245,12 @@ function Thumb({
<div className="w-full h-full" />
)}
{video.duration_seconds != null ? (
<span className="absolute bottom-1.5 right-1.5 bg-black/80 text-white text-[11px] font-medium px-1.5 py-0.5 rounded-sm">
<span className="absolute bottom-1.5 right-1.5 bg-black/80 text-white text-2xs font-medium px-1.5 py-0.5 rounded-sm">
{formatDuration(video.duration_seconds)}
</span>
) : video.live_status === "live" || video.live_status === "upcoming" ? (
<span
className={`absolute bottom-1.5 right-1.5 text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
className={`absolute bottom-1.5 right-1.5 text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
video.live_status === "live" ? "bg-red-500/90 text-white" : "bg-black/80 text-white"
}`}
>
@@ -258,7 +258,7 @@ function Thumb({
</span>
) : null}
{video.live_status === "was_live" && (
<span className="absolute top-1.5 left-1.5 bg-accent text-accent-fg text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
<span className="absolute top-1.5 left-1.5 bg-accent text-accent-fg text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded-sm">
{t("card.stream")}
</span>
)}
+1 -1
View File
@@ -487,7 +487,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
...tileStyle(Math.floor(hoverTime / (strip.interval_s || 1))),
}}
/>
<div className="text-[10px] text-center text-muted py-0.5 tabular-nums">
<div className="text-3xs text-center text-muted py-0.5 tabular-nums">
{tc(hoverTime)}
</div>
</div>
+2 -2
View File
@@ -554,7 +554,7 @@ function AuthCard() {
>
{t("welcome.auth.resendLink")}
</button>
<div className="flex items-center gap-3 my-4 text-[11px] uppercase tracking-wide text-muted">
<div className="flex items-center gap-3 my-4 text-2xs uppercase tracking-wide text-muted">
<span className="h-px flex-1 bg-border" />
{t("welcome.auth.or")}
<span className="h-px flex-1 bg-border" />
@@ -576,7 +576,7 @@ function AuthCard() {
</button>
) : (
<form onSubmit={onDemo} className="mt-2 flex flex-col gap-2">
<p className="text-[11px] text-muted">{t("welcome.auth.demoHint")}</p>
<p className="text-2xs text-muted">{t("welcome.auth.demoHint")}</p>
<div className="flex gap-2">
<input
type="email"
+1 -1
View File
@@ -86,7 +86,7 @@ export function auditColumns(
// card (the row rules bled into the page). Wrapping keeps every row inside the container.
<div className="min-w-0">
{r.summary && <div className="wrap-break-word">{r.summary}</div>}
{change && <div className="text-[11px] text-muted wrap-break-word">{change}</div>}
{change && <div className="text-2xs text-muted wrap-break-word">{change}</div>}
</div>
);
},
+4 -4
View File
@@ -69,7 +69,7 @@ export function playlistColumns({
</span>
</button>
<button onClick={() => onPlay(v)} className="min-w-0 flex-1 text-left">
<span className="block text-[13px] text-fg truncate">{v.title}</span>
<span className="block text-[0.8125rem] text-fg truncate">{v.title}</span>
</button>
</div>
),
@@ -83,7 +83,7 @@ export function playlistColumns({
sortValue: (v) => v.channel_title ?? "",
filter: { kind: "text", get: (v) => v.channel_title ?? "" },
render: (v) => (
<span className="text-[13px] text-muted truncate block">{v.channel_title}</span>
<span className="text-[0.8125rem] text-muted truncate block">{v.channel_title}</span>
),
},
{
@@ -99,12 +99,12 @@ export function playlistColumns({
sortValue: (v) => v.duration_seconds ?? Number.MAX_SAFE_INTEGER,
render: (v) =>
v.duration_seconds != null ? (
<span className="text-[11px] text-muted tabular-nums">
<span className="text-2xs text-muted tabular-nums">
{formatDuration(v.duration_seconds)}
</span>
) : v.live_status === "live" || v.live_status === "upcoming" ? (
<span
className={`text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
className={`text-3xs font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
v.live_status === "live"
? "bg-red-500/90 text-white" /* fixed live-red, not the status token */
: "bg-card border border-border text-muted"
+14 -4
View File
@@ -43,6 +43,15 @@
--z-index-tuner: 9999;
}
/* Extra-small text steps in rem, so these labels scale with --font-scale like every other
text (the ~124 px-frozen text-[10/11px] labels did NOT). Font-size ONLY — no paired
--text-*--line-height — to match the `text-[Npx]` arbitraries they replace and avoid any
vertical-rhythm shift. */
@theme {
--text-2xs: 0.6875rem; /* 11px @ base — the most common frozen label */
--text-3xs: 0.625rem; /* 10px @ base */
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
@@ -79,10 +88,11 @@
:root {
--font-scale: 1.06;
/* Vertical space the floating top header needs to clear (top offset 12px + pill height 44px +
bottom breathing room ~12px). Module content pads its top by this so nothing hides under the
header; kept in one place since App and Playlists both reference it. */
--hdr-h: 68px;
/* Vertical space the floating top header needs to clear (top offset ~0.75rem + pill height ~2.75rem
+ bottom breathing room ~0.75rem). In rem so it grows with --font-scale in lock-step with the
rem-sized header pills — a px value let content slide under the header at scale 1.3. Kept in one
place since App and Playlists both reference it. */
--hdr-h: 4.25rem;
/* ===== Glass tunables — every value that materially drives the look lives here so it can be
tuned in ONE place. ===== */