tracker
All repositories: gitoria
25.0 KB
// styles.hl — all of the app's CSS, served as /__hl/app.css. Components carry no Style// blocks: markup is domain tags, this file lays them out.//// TOKENS: the WorldAPI palette + semantic tokens are declared in `shared/tokens.hl` as// hl:web css variables, vendored verbatim from ident.worldapi.org (see that project's// README "Design tokens"). This file imports the tokens it uses and writes them as members;// the app sets only its accent (greenDark, per CONCEPT.md / antcolony README "look and style").//// In a styles file a bare name is a TAG (camelCase -> kebab: `emptyState` is `empty-state`),// a quoted name is a selector written verbatim. MOBILE FIRST (mission 047, the creator: "style is// supposed to be mobile first"): every rule outside a block is the PHONE's (390 px wide); the// `@media (min-width: 40rem)` block at the bottom only ADDS what a wider screen changes. Nothing// here may make a 390 px page scroll sideways, and the header stays ONE row (the gate checks both).import { var } from 'hl:web/css'import { greenDark, colorText, colorBackground, colorTextStrong, colorBackgroundSunken, colorTextMuted, colorAccentText, colorBorder, colorDanger, colorSurface, orange, blueDark } from './shared/tokens.hl'colorAccent = var(greenDark) // the app's accent: the only token this app sets// tracker.worldapi.org#4: genre pills in blue; artists orange (mission 047, the creator: "orange, not red") — the raw// palette tokens (like the accent above), not the semantic `colorDanger`/`colorAction` (this isn't a// warning or an action, just the creator's chosen colour for these two kinds of link/pill)colorArtist = var(orange)colorGenre = var(blueDark)'*,*::before,*::after' { boxSizing = 'border-box' }html { colorScheme = 'dark' }body { margin = '0' font = '16px/1.5 system-ui, sans-serif' color = colorText background = colorBackground }a { color = colorAccent textDecoration = 'none' }'a:hover' { textDecoration = 'underline' }h1 { margin = '0 0 .8rem' fontSize = '1.2rem' lineHeight = '1.3' color = colorTextStrong }button { font = 'inherit' fontWeight = '600' padding = '.4rem .9rem' color = colorAccentText background = colorAccent border = '0' borderRadius = '.4rem' cursor = 'pointer' }'button:hover' { filter = 'brightness(1.1)' }'button.quiet' { color = colorText background = 'transparent' border = '0' }// the ONLY buttons with a border are inverted ones (creator: "no borders for the buttons unless they are inverted")'button.inverted' { color = colorAccent background = 'transparent' border = '1px solid ' + colorAccent }'button.quiet:hover' { background = colorSurface filter = 'none' }// the header is ONE row on a phone: brand left, ident selector + "Log out" right, never wrapping — the// user box may shrink (minWidth 0), the brand may notapplicationHeader { position = 'sticky' top = '0' zIndex = '5' display = 'flex' flexWrap = 'nowrap' alignItems = 'center' gap = '.5rem' padding = '.45rem .75rem' background = colorBackgroundSunken }'application-header .brand' { flex = 'none' fontWeight = '100' fontSize = '1.15rem' lineHeight = '1.2' color = colorAccent }main { display = 'block' maxWidth = '60rem' margin = '0 auto' padding = '.8rem .75rem' }// ---- login (top right of the header), copied from calendar.worldapi.org's styles.hl -------------userBox { display = 'flex' alignItems = 'center' gap = '.25rem' marginLeft = 'auto' minWidth = '0' }'#logout' { flex = 'none' padding = '.3rem .5rem' fontSize = '.9rem' }'a.button' { display = 'inline-block' padding = '.35rem .8rem' fontWeight = '600' color = colorAccentText background = colorAccent borderRadius = '.4rem' }'a.button:hover' { textDecoration = 'none' filter = 'brightness(1.1)' }identSelector { '--ident-accent' = colorAccent '--ident-accent-text' = colorAccentText '--ident-background' = colorSurface '--ident-border' = colorBorder '--ident-font' = '600 .85rem/1.4 system-ui, sans-serif' minWidth = '0' }'ident-selector::part(button)' { padding = '.3rem .6rem' border = 'none' whiteSpace = 'nowrap' }'ident-selector::part(status)' { padding = '.3rem .6rem' border = 'none' }'ident-selector::part(identity)' { border = 'none' }// signed out on a phone (mission 056): the selector (nowrap above) and "Log in" never wrap their text — one header row down// to 320 px (a selector key may appear only ONCE per block: a second one replaces the first)'#loginbutton' { flex = 'none' whiteSpace = 'nowrap' padding = '.3rem .6rem' fontSize = '.85rem' }'#loginbutton .wide' { display = 'none' }'.message' { color = colorDanger }'.muted' { color = colorTextMuted }'.message.banner' { margin = '0' padding = '.5rem .75rem' background = colorBackgroundSunken }'p.message:empty' { display = 'none' }// ---- offline (tracker.worldapi.org#10, components/main.hl): the note under the header, and the probe whose endless// animation is the shell's 1 s tick (it must be rendered for the animation to run — not display none — so: invisible)offlineNote { display = 'block' padding = '.5rem .75rem' color = colorTextStrong background = colorSurface }netProbe { position = 'fixed' left = '0' top = '0' width = '1px' height = '1px' opacity = '0' pointerEvents = 'none' animation = 'tracker-net-tick 1s linear infinite' }'@keyframes tracker-net-tick' = {'from' = { opacity = '0' }to = { opacity = '0' }}// ---- /shows/:slug (tracker.worldapi.org#4) --------------------------------------------------showPage { display = 'block' }// phone (mission 047, the creator): the title (+ Follow) first, then the poster at the full content width, then genres,// plot, cast — `showMeta` dissolves (display contents) so its title row can go before the poster (order -1). From 40rem// the creator's desktop layout: poster left (10rem), right of it the title and the rest.showHeader { display = 'flex' flexDirection = 'column' gap = '.6rem' marginBottom = '1.2rem' }showPoster { display = 'block' width = '100%' }'show-poster img' { display = 'block' width = '100%' height = 'auto' borderRadius = '.4rem' aspectRatio = '2 / 3' objectFit = 'cover' background = colorSurface }showMeta { display = 'contents' overflowWrap = 'anywhere' }showTitle { order = '-1' display = 'flex' alignItems = 'center' gap = '.6rem' }'show-title h1' { flex = '1' minWidth = '0' margin = '0' }'button.follow' { flex = 'none' padding = '.3rem .8rem' fontSize = '.9rem' }'button.follow.inverted' { padding = 'calc(.3rem - 1px) calc(.8rem - 1px)' }// tracker.worldapi.org#12: the show on TMDB / IMDb / TheTVDB / TVmaze — small monochrome badges (CSS only, no copied// logos), muted like the other secondary text, the accent on hover. Phone: right under the title row (order -1, after it// in the markup), above the full-width poster.showLinks { order = '-1' display = 'flex' flexWrap = 'wrap' gap = '.4rem' }'a.extlink' { display = 'inline-flex' alignItems = 'center' height = '1.5rem' padding = '0 .45rem' borderRadius = '.3rem' font = '700 .75rem/1 system-ui, sans-serif' letterSpacing = '.02em' color = colorBackground background = colorTextMuted }'a.extlink:hover' { textDecoration = 'none' background = colorAccent }showGenres { display = 'flex' flexWrap = 'wrap' gap = '.4rem' margin = '.5rem 0' }'.genre-pill' { display = 'inline-block' padding = '.15rem .6rem' borderRadius = '1rem' fontSize = '.85rem' color = colorGenre border = '1px solid ' + colorGenre }showSummary { display = 'block' margin = '.5rem 0' fontSize = '.95rem' color = colorText }// tracker.worldapi.org#28: crew (one small muted label per job, the names after it) and the whole cast — "Name (Character)",// comma-separated as before; names past the first 20 (a crew job: 6) are hidden while the block is `collapsed`, the "Show all"// button (no border: not inverted) flips the block's class (client-side). The character and the comma are the link's ::after// (`data-role`): one element per name, so no SSR line break can put a space before the comma.showCrew { display = 'block' margin = '.5rem 0' }showCast { display = 'block' margin = '.5rem 0' }castHeading { display = 'block' fontSize = '.75rem' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }crewLabel { display = 'block' marginTop = '.35rem' fontSize = '.75rem' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }'show-crew crew-label:first-child' { marginTop = '0' }castLabel { display = 'block' margin = '.4rem 0 .1rem' fontSize = '.75rem' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }'.artist-link' { color = colorArtist }'.crew-link' { color = colorArtist }'a.artist-link::after' { content = "attr(data-role) ', '" color = colorTextMuted }'a.crew-link::after' { content = "', '" color = colorTextMuted }'a.artist-link.end::after,show-cast.collapsed a.artist-link.cut::after' { content = 'attr(data-role)' }'a.crew-link.end::after,show-crew.collapsed a.crew-link.cut::after' { content = 'none' }'show-cast.collapsed .more,show-crew.collapsed .more' { display = 'none' }'button.showall' { display = 'block' margin = '.3rem 0 0' padding = '.15rem 0' fontSize = '.85rem' color = colorAccent background = 'transparent' }'button.showall:hover' { background = 'transparent' textDecoration = 'underline' }'show-cast.collapsed when-all,show-crew.collapsed when-all,show-cast.all when-collapsed,show-crew.all when-collapsed' { display = 'none' }showSeasons { display = 'block' }seasonRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.6rem 0' borderTop = '1px solid ' + colorBorder }'button.season-toggle' { flex = '1' minWidth = '0' display = 'flex' alignItems = 'baseline' gap = '.6rem' justifyContent = 'flex-start' padding = '0' color = colorText background = 'transparent' border = '0' fontWeight = '600' cursor = 'pointer' textAlign = 'left' }seasonCount { fontWeight = '400' fontSize = '.85rem' color = colorTextMuted }// the caret at the header's right edge (mission 047): DOWN while closed, UP while open (tracker#17 — was right/down)seasonCaret { flex = 'none' alignSelf = 'center' display = 'grid' marginLeft = 'auto' color = colorTextMuted transition = 'transform .15s' }'season-caret svg' { display = 'block' fill = 'none' stroke = 'currentColor' strokeWidth = '2' strokeLinecap = 'round' strokeLinejoin = 'round' }'season-row.expanded season-caret' { transform = 'rotate(180deg)' }// episodes are NOT indented (mission 047): an episode's check sits under its season's checkepisodeRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.3rem 0' }episodeNumber { flex = 'none' width = '1.6rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }episodeTitle { flex = '1' minWidth = '0' overflowWrap = 'anywhere' }// tracker#17: a season's placeholder rows while its episodes load — the episode row's shape (check disc, number, title) in// a moving gradient (the shimmer); same height as an episode row so nothing jumps when the real rows replace themskeletonRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.3rem 0' }'skeleton-check, skeleton-number, skeleton-title' { display = 'block' borderRadius = '.3rem' background = 'linear-gradient(90deg, ' + colorSurface + ' 35%, ' + colorBorder + ' 50%, ' + colorSurface + ' 65%)' backgroundSize = '400% 100%' animation = 'tracker-shimmer 1.4s linear infinite' }skeletonCheck { flex = 'none' width = '2rem' height = '2rem' borderRadius = '50%' }skeletonNumber { flex = 'none' width = '1.6rem' height = '1rem' }skeletonTitle { flex = '1' maxWidth = '60%' height = '1rem' }'skeleton-row:nth-of-type(even) skeleton-title' { maxWidth = '42%' }// tracker#26: an incomplete title while its details load (components/show.hl) — the poster's place (2:3, the poster's// width), a cast line, and season-shaped rows; the same shimmer'skeleton-poster, skeleton-cast' { display = 'block' borderRadius = '.3rem' background = 'linear-gradient(90deg, ' + colorSurface + ' 35%, ' + colorBorder + ' 50%, ' + colorSurface + ' 65%)' backgroundSize = '400% 100%' animation = 'tracker-shimmer 1.4s linear infinite' }skeletonPoster { width = '100%' aspectRatio = '2 / 3' }skeletonCast { width = '70%' height = '1rem' margin = '.5rem 0' }showLoading { display = 'block' marginTop = '1rem' }// its tick (like personProbe): rendered, invisibleshowProbe { position = 'fixed' left = '0' top = '0' width = '1px' height = '1px' opacity = '0' pointerEvents = 'none' animation = 'tracker-person-tick .1s linear infinite' }'@keyframes tracker-shimmer' = {'from' = { backgroundPosition = '100% 0' }to = { backgroundPosition = '0% 0' }}// the check: "a disc with a check in it" (drawn in CSS/SVG, no icon font) — solid (filled accent disc, dark check)// once watched; while not, its INVERSION (mission 047, the creator): an accent outline circle AND an accent outline check'button.checkicon' { flex = 'none' display = 'grid' placeItems = 'center' width = '2rem' height = '2rem' padding = '0' background = 'transparent' border = '0' borderRadius = '50%' cursor = 'pointer' }'.checkicon circle' { fill = 'transparent' stroke = colorAccent strokeWidth = '1.5' }'.checkicon path' { fill = 'none' stroke = colorAccent strokeWidth = '2' strokeLinecap = 'round' strokeLinejoin = 'round' }'.checkicon.solid circle' { fill = colorAccent stroke = colorAccent }'.checkicon.solid path' { stroke = colorAccentText }// ---- /unwatched (tracker.worldapi.org#5) ----------------------------------------------------unwatchedPage { display = 'block' }unwatchedRows { display = 'block' }// phone: two lines next to the check — show title + date, below it the episode; one line from 40remunwatchedRow { display = 'grid' gridTemplateColumns = '2rem minmax(0, 1fr) auto' alignItems = 'center' gap = '0 .6rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }'unwatched-row button.checkicon' { gridRow = 'span 2' }'.unwatched-show' { fontWeight = '600' overflowWrap = 'anywhere' }unwatchedEpisode { gridColumn = '2 / 4' gridRow = '2' color = colorText overflowWrap = 'anywhere' }unwatchedDate { gridRow = '1' gridColumn = '3' fontSize = '.85rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }// ---- /schedule (tracker.worldapi.org#6) — same row layout as /unwatched, minus the check icon -----schedulePage { display = 'block' }scheduleRows { display = 'block' }scheduleRow { display = 'grid' gridTemplateColumns = 'minmax(0, 1fr) auto' alignItems = 'center' gap = '0 .6rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }'.schedule-show' { fontWeight = '600' overflowWrap = 'anywhere' }scheduleEpisode { gridColumn = '1 / 3' gridRow = '2' color = colorText overflowWrap = 'anywhere' }scheduleDate { gridRow = '1' gridColumn = '2' fontSize = '.85rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }// ---- /my/shows (tracker.worldapi.org#7) — small poster, title, last watched episode ------------myShowsPage { display = 'block' }myShowsRows { display = 'block' }myShowRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }'img.myshow-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }'.myshow-title' { flex = '1' minWidth = '0' fontWeight = '600' overflowWrap = 'anywhere' }myShowEpisode { flex = 'none' fontSize = '.9rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }// ---- / (tracker.worldapi.org#13): signed in, four big icon tiles; then the text and two poster rows ----------------home { display = 'block' }homeNav { display = 'grid' gridTemplateColumns = 'repeat(2, minmax(0, 1fr))' gap = '.6rem' marginBottom = '1.2rem' }'a.hometile' { display = 'flex' flexDirection = 'column' alignItems = 'center' justifyContent = 'center' gap = '.4rem' padding = '1rem .5rem' borderRadius = '.6rem' color = colorTextStrong background = colorSurface fontWeight = '600' }'a.hometile:hover' { textDecoration = 'none' color = colorAccent }'a.hometile svg' { display = 'block' width = '2.5rem' height = '2.5rem' fill = 'none' stroke = colorAccent strokeWidth = '1.6' strokeLinecap = 'round' strokeLinejoin = 'round' }homeIntro { display = 'block' marginBottom = '1rem' color = colorText }'home-intro p' { margin = '0 0 .6rem' }homeSection { display = 'block' marginBottom = '1.2rem' }'home-section h2' { margin = '0 0 .5rem' fontSize = '1.1rem' }'home-section h2 a' { color = colorTextStrong }'home-section h2 a::after' { content = "' ›'" color = colorAccent }// phone: the row scrolls sideways INSIDE itself (the page never does); from 40rem a grid of 5tileRow { display = 'flex' gap = '.6rem' overflowX = 'auto' scrollSnapType = 'x proximity' paddingBottom = '.4rem' }'tile-row a.tile' { flex = 'none' width = '7rem' scrollSnapAlign = 'start' }// /movies and /shows: a grid of 3 on a phone, 6 from 40remlistPage { display = 'block' }'.list-count' { margin = '-.4rem 0 .8rem' fontSize = '.9rem' }tileGrid { display = 'grid' gridTemplateColumns = 'repeat(3, minmax(0, 1fr))' gap = '.8rem .6rem' marginBottom = '1rem' }pagerBox { display = 'block' margin = '1rem 0' }// the copied pagination (components/pagination.hl, its own Style) a bit tighter on a phone, so "Previous 1 … 4 5 6 … 326// Next" stays one row at 390 px; from 40rem its own sizes'pager-box pagination ol' { gap = '.1rem' }'pager-box pagination button' { minWidth = '1.8rem' padding = '.3rem .3rem' fontSize = '.95rem' }// a tile: the poster (2:3, the placeholder until the sync has a real one), the title (two lines at most), the year'a.tile' { display = 'block' minWidth = '0' color = colorText }'a.tile:hover' { textDecoration = 'none' }'a.tile:hover tile-title' { color = colorAccent }'img.tile-poster' { display = 'block' width = '100%' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.35rem' background = colorSurface }tileTitle { display = '-webkit-box' marginTop = '.3rem' fontSize = '.85rem' fontWeight = '600' lineHeight = '1.25' overflow = 'hidden' overflowWrap = 'anywhere' '-webkit-line-clamp' = '2' '-webkit-box-orient' = 'vertical' }tileYear { display = 'block' fontSize = '.8rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }myMoviesPage { display = 'block' }// ---- the search (tracker.worldapi.org#14) — the header's magnifier and /search ------------------------------------'a.searchlink' { flex = 'none' display = 'grid' placeItems = 'center' width = '2rem' height = '2rem' color = colorText borderRadius = '.4rem' }'a.searchlink:hover' { color = colorAccent background = colorSurface }'a.searchlink svg' { display = 'block' fill = 'none' stroke = 'currentColor' strokeWidth = '2' strokeLinecap = 'round' }searchPage { display = 'block' }searchBar { display = 'block' marginBottom = '.8rem' }'#searchinput' { display = 'block' width = '100%' padding = '.55rem .75rem' font = 'inherit' color = colorTextStrong background = colorSurface border = '0' borderRadius = '.4rem' outline = 'none' }'#searchinput:focus' { boxShadow = '0 0 0 2px ' + colorAccent }searchSection { display = 'block' marginBottom = '1rem' }'search-section h2' { margin = '.4rem 0 .2rem' fontSize = '.85rem' fontWeight = '600' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }searchRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.4rem 0' borderTop = '1px solid ' + colorBorder }webRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.4rem 0' borderTop = '1px solid ' + colorBorder }'img.search-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }'.search-title' { flex = '1' minWidth = '0' fontWeight = '600' overflowWrap = 'anywhere' }'.search-person' { flex = '1' minWidth = '0' color = colorArtist overflowWrap = 'anywhere' }searchType { flex = 'none' fontSize = '.8rem' color = colorTextMuted }webText { flex = '1' minWidth = '0' display = 'flex' flexDirection = 'column' }webTitle { fontWeight = '600' color = colorTextStrong overflowWrap = 'anywhere' }'button.add' { flex = 'none' padding = 'calc(.3rem - 1px) calc(.8rem - 1px)' fontSize = '.9rem' }webBar { display = 'flex' alignItems = 'center' gap = '.8rem' margin = '.6rem 0' }// ---- /person/<slug> (tracker.worldapi.org#16): photo + facts + bio, then the filmography rows ----------------------// the photo left of the name (phone 6rem, from 40rem 10rem). Rows like the search's: small poster, title, year · kind,// the role belowpersonPage { display = 'block' }personHeader { display = 'flex' alignItems = 'flex-start' gap = '.8rem' marginBottom = '1rem' }personPhoto { flex = 'none' display = 'block' width = '6rem' }'person-photo img' { display = 'block' width = '100%' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.4rem' background = colorSurface }personMeta { display = 'block' minWidth = '0' overflowWrap = 'anywhere' }personFact { display = 'block' fontSize = '.95rem' color = colorTextMuted }// the bio under photo + facts, full width; four lines until clicked (components/person.hl toggleBio)personBio { display = 'block' margin = '0 0 1rem' fontSize = '.95rem' whiteSpace = 'pre-line' cursor = 'pointer' }'person-bio.short' { display = '-webkit-box' overflow = 'hidden' '-webkit-line-clamp' = '4' '-webkit-box-orient' = 'vertical' }'h2.film-heading' { margin = '.4rem 0 .2rem' fontSize = '.85rem' fontWeight = '600' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }personLoading { display = 'block' padding = '.6rem 0' color = colorTextMuted animation = 'tracker-person-pulse 1.2s ease-in-out infinite alternate' }// the fill's tick (components/person.hl): rendered (an animation needs it) but invisible, like netProbepersonProbe { position = 'fixed' left = '0' top = '0' width = '1px' height = '1px' opacity = '0' pointerEvents = 'none' animation = 'tracker-person-tick .1s linear infinite' }'@keyframes tracker-person-tick' = {'from' = { opacity = '0' }to = { opacity = '0' }}'@keyframes tracker-person-pulse' = {'from' = { opacity = '1' }to = { opacity = '.45' }}filmRows { display = 'block' }'a.film-row' { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.45rem 0' color = colorText borderTop = '1px solid ' + colorBorder }'a.film-row:hover' { textDecoration = 'none' }'a.film-row:hover film-title' { color = colorAccent }'img.film-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }filmText { flex = '1' minWidth = '0' display = 'flex' flexDirection = 'column' }filmTitle { fontWeight = '600' color = colorTextStrong overflowWrap = 'anywhere' }filmMeta { display = 'flex' gap = '.5rem' fontSize = '.8rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }filmRole { fontSize = '.85rem' color = colorArtist overflowWrap = 'anywhere' }'film-role:empty' { display = 'none' }'film-year:empty' { display = 'none' }// ---- wider screens (≥ 40rem = 640 px): only what changes from the phone -------------------------'@media (min-width: 40rem)' = {applicationHeader = { gap = '1rem' padding = '.6rem 1rem' }userBox = { gap = '.5rem' }'#logout' = { padding = '.4rem .9rem' fontSize = '1rem' }identSelector = { '--ident-font' = '600 .9rem/1.4 system-ui, sans-serif' }'ident-selector::part(button)' = { padding = '.35rem .8rem' }'ident-selector::part(status)' = { padding = '.35rem .8rem' }'#loginbutton' = { padding = '.35rem .8rem' fontSize = '1rem' }'#loginbutton .wide' = { display = 'inline' }main = { padding = '1rem' }h1 = { margin = '0 0 1rem' fontSize = '1.3rem' }showHeader = { flexDirection = 'row' alignItems = 'flex-start' gap = '1rem' marginBottom = '1.5rem' }showPoster = { flex = 'none' width = '10rem' }showMeta = { display = 'block' flex = '1' minWidth = '0' }showTitle = { marginBottom = '1rem' }showSummary = { fontSize = '1rem' }episodeRow = { padding = '.4rem 0' }skeletonRow = { padding = '.4rem 0' }unwatchedRow = { display = 'flex' }'.unwatched-show' = { flex = 'none' }unwatchedEpisode = { flex = '1' }unwatchedDate = { flex = 'none' }scheduleRow = { display = 'flex' }'.schedule-show' = { flex = 'none' }scheduleEpisode = { flex = '1' }scheduleDate = { flex = 'none' }homeNav = { gridTemplateColumns = 'repeat(4, minmax(0, 1fr))' gap = '1rem' }'a.hometile' = { padding = '1.4rem .5rem' }tileRow = { display = 'grid' gridTemplateColumns = 'repeat(5, minmax(0, 1fr))' gap = '1rem .8rem' overflowX = 'visible' }'tile-row a.tile' = { width = 'auto' }tileGrid = { gridTemplateColumns = 'repeat(6, minmax(0, 1fr))' gap = '1rem .8rem' }'pager-box pagination ol' = { gap = '.3rem' }'pager-box pagination button' = { minWidth = '2.2rem' padding = '.35rem .6rem' fontSize = '1rem' }personHeader = { gap = '1rem' }personPhoto = { width = '10rem' }}
Branches
- mainmain branch
Latest commits
- 10bb3f93tracker#28 (mission 061): full cast (all seasons, main cast by episodes, guest stars) + crew (created by, directed by, written by, screenplay, story, music) — stored by the details completion, the daily sync, the search import (one details request) and a background credits job (resumes, RSS limit); show page collapsed after 20 with client-side Show all; showBySlug via a slug map; gate 249, tests/realdata-028.mjs, README + STATUSmre
- 25a50bc4tracker#26 (mission 059): titles from a filmography are completed — on open (skeleton, step-wise face showComplete, no reload) and by the in-app details repair (resumes, TMDB-paced, series in parts); cast from TMDB credits; gate 231, tests/realdata-026.mjs, README + STATUSmre
- 1704ec45tracker#17 (mission 057): season caret down/up, skeleton rows while a season loads, sessionless showSeasonEpisodes face (no page re-mount), client-only close; gate 214, tests/realdata-057.mjs, README + STATUSmre
- f2fe3e36mission 056: README + STATUS (merge, fixes, Hybriel 8590df63, real-data check), tests/realdata-056.mjs, tools/check-public-slugs.hlmre
- f40c250emission 056: re-vendor hybriel master 8590df63 (#121, #122); an adult title's page is Not found for non-followers; gate: leave the page before stopping the servermre
- 2b7fdd6cmission 056: signed-out header one row on phones ("Log in", nowrap), backfill skips adult titles' posters, gate checksmre
- 2c53d5efMerge branch 't16-person' (tracker#16 person pages) into main; filmography shows only public titles (054 adult flag), gate race fix (backfill start line)mre
- c171227emission 054: hide adult/unknown titles from the public lists and the search; in-app adult-flag backfill (TMDB details + poster per title, resumes), gate + real-data proofmre
- 139fafd8tracker#16: short bio (4 lines, click = all), real-data check script, README + STATUSmre
- 93be9476tracker#16: person pages /person/<slug> with the filmography fetched from TMDB on the first visit (step by step), gatemre
- 47a3cae6STATUS: mission 053 merge commit idsmre
- dcc5eecaMerge branch 't14-search'mre
- 03edc783Merge branch 't15-tvmaze'mre
- 71b46345tracker#15: numbering check by date or title, placeholder titles in other languages, docs + real-data proofmre
- 6bb2daf1tracker#13: homepage (tiles, intro, latest movies/shows), /shows, /movies/page/N, /my/movies; lists cached in memorymre
- b8bd1157tracker#14: README + STATUS (search, real-data numbers, gate, merge notes)mre
- 65c694a8tracker#14: search — header magnifier, /search/<text> (in-memory word-prefix index over titles + people), Fetch from web (TMDB search/multi, ours left out), Add = import via syncShow; gate +25 checks, real-data scriptmre
- 34f2c15btracker#15: TVmaze merge in the sync (gaps only: new episodes/seasons, empty titles/air dates; numbering check), fake TVmaze episodes + gatemre
- cbdc4ea7tracker#12: link icons TMDB/IMDb/TVDB/TVmaze; sync fills missing ids (TVmaze lookup); movies fetched via /movie/mre
- b105bcd8tracker#11: Hybriel master ff51cf46 (checks no longer vanish), mobile-first styles, carets, follow button, sign-in modal, inverted check, orange castmre