gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit749019b0749019b0Merge t19 (tracker#19 franchises + timelines) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs/faketmdb.mjs, both sides kept; franchise/timeline pages get #20's typed heading (Franchise | …, Timeline | …), their title links via titlePath (/movies|/series|/shows); the collection seed waits for repair, kinds and credits too; franchises.mjs URLs + 2 new checks; gates browser 323/0, kinds 32/0, franchises 50/0mre749019b0/styles.hl

34.0 KB

  1. // styles.hl — all of the app's CSS, served as /__hl/app.css. Components carry no Style
  2. // blocks: markup is domain tags, this file lays them out.
  3. //
  4. // TOKENS: the WorldAPI palette + semantic tokens are declared in `shared/tokens.hl` as
  5. // hl:web css variables, vendored verbatim from ident.worldapi.org (see that project's
  6. // README "Design tokens"). This file imports the tokens it uses and writes them as members;
  7. // the app sets only its accent (greenDark, per CONCEPT.md / antcolony README "look and style").
  8. //
  9. // In a styles file a bare name is a TAG (camelCase -> kebab: `emptyState` is `empty-state`),
  10. // a quoted name is a selector written verbatim. MOBILE FIRST (mission 047, the creator: "style is
  11. // supposed to be mobile first"): every rule outside a block is the PHONE's (390 px wide); the
  12. // `@media (min-width: 40rem)` block at the bottom only ADDS what a wider screen changes. Nothing
  13. // here may make a 390 px page scroll sideways, and the header stays ONE row (the gate checks both).
  14. import { var } from 'hl:web/css'
  15. import { greenDark, colorText, colorBackground, colorTextStrong, colorBackgroundSunken, colorTextMuted, colorAccentText, colorBorder, colorDanger, colorSurface, orange, blueDark, yellow, purple, blueLight, greenLight } from './shared/tokens.hl'
  16. colorAccent = var(greenDark) // the app's accent: the only token this app sets
  17. // tracker.worldapi.org#4: genre pills in blue; artists orange (mission 047, the creator: "orange, not red") — the raw
  18. // palette tokens (like the accent above), not the semantic `colorDanger`/`colorAction` (this isn't a
  19. // warning or an action, just the creator's chosen colour for these two kinds of link/pill)
  20. colorArtist = var(orange)
  21. colorGenre = var(blueDark)
  22. // tracker.worldapi.org#20 (mission 068): every TYPE has one palette colour, wherever its word appears (page headings, list
  23. // headings, the homepage rows, the search's and the filmography's labels): Movie yellow, Series the accent (greenDark), Show
  24. // purple, Genre blue and Person orange (as the pills and artist links already were), Franchise light blue, Timeline light
  25. // green (tracker#19's pages). The name next to it stays white (colorTextStrong).
  26. colorMovie = var(yellow)
  27. colorSeries = var(greenDark)
  28. colorShow = var(purple)
  29. colorFranchise = var(blueLight)
  30. colorTimeline = var(greenLight)
  31. '*,
  32. *::before,
  33. *::after' { boxSizing = 'border-box' }
  34. html { colorScheme = 'dark' }
  35. body { margin = '0' font = '16px/1.5 system-ui, sans-serif' color = colorText background = colorBackground }
  36. a { color = colorAccent textDecoration = 'none' }
  37. 'a:hover' { textDecoration = 'underline' }
  38. h1 { margin = '0 0 .8rem' fontSize = '1.2rem' lineHeight = '1.3' color = colorTextStrong }
  39. button { font = 'inherit' fontWeight = '600' padding = '.4rem .9rem' color = colorAccentText background = colorAccent border = '0' borderRadius = '.4rem' cursor = 'pointer' }
  40. 'button:hover' { filter = 'brightness(1.1)' }
  41. 'button.quiet' { color = colorText background = 'transparent' border = '0' }
  42. // the ONLY buttons with a border are inverted ones (creator: "no borders for the buttons unless they are inverted")
  43. 'button.inverted' { color = colorAccent background = 'transparent' border = '1px solid ' + colorAccent }
  44. 'button.quiet:hover' { background = colorSurface filter = 'none' }
  45. // the header is ONE row on a phone: brand left, ident selector + "Log out" right, never wrapping — the
  46. // user box may shrink (minWidth 0), the brand may not
  47. applicationHeader { position = 'sticky' top = '0' zIndex = '5' display = 'flex' flexWrap = 'nowrap' alignItems = 'center' gap = '.5rem' padding = '.45rem .75rem' background = colorBackgroundSunken }
  48. 'application-header .brand' { flex = 'none' fontWeight = '100' fontSize = '1.15rem' lineHeight = '1.2' color = colorAccent }
  49. main { display = 'block' maxWidth = '60rem' margin = '0 auto' padding = '.8rem .75rem' }
  50. // ---- login (top right of the header), copied from calendar.worldapi.org's styles.hl -------------
  51. userBox { display = 'flex' alignItems = 'center' gap = '.25rem' marginLeft = 'auto' minWidth = '0' }
  52. '#logout' { flex = 'none' padding = '.3rem .5rem' fontSize = '.9rem' }
  53. 'a.button' { display = 'inline-block' padding = '.35rem .8rem' fontWeight = '600' color = colorAccentText background = colorAccent borderRadius = '.4rem' }
  54. 'a.button:hover' { textDecoration = 'none' filter = 'brightness(1.1)' }
  55. 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' }
  56. 'ident-selector::part(button)' { padding = '.3rem .6rem' border = 'none' whiteSpace = 'nowrap' }
  57. 'ident-selector::part(status)' { padding = '.3rem .6rem' border = 'none' }
  58. 'ident-selector::part(identity)' { border = 'none' }
  59. // signed out on a phone (mission 056): the selector (nowrap above) and "Log in" never wrap their text — one header row down
  60. // to 320 px (a selector key may appear only ONCE per block: a second one replaces the first)
  61. '#loginbutton' { flex = 'none' whiteSpace = 'nowrap' padding = '.3rem .6rem' fontSize = '.85rem' }
  62. '#loginbutton .wide' { display = 'none' }
  63. '.message' { color = colorDanger }
  64. '.muted' { color = colorTextMuted }
  65. '.message.banner' { margin = '0' padding = '.5rem .75rem' background = colorBackgroundSunken }
  66. 'p.message:empty' { display = 'none' }
  67. // ---- offline (tracker.worldapi.org#10, components/main.hl): the note under the header, and the probe whose endless
  68. // animation is the shell's 1 s tick (it must be rendered for the animation to run — not display none — so: invisible)
  69. offlineNote { display = 'block' padding = '.5rem .75rem' color = colorTextStrong background = colorSurface }
  70. netProbe { position = 'fixed' left = '0' top = '0' width = '1px' height = '1px' opacity = '0' pointerEvents = 'none' animation = 'tracker-net-tick 1s linear infinite' }
  71. '@keyframes tracker-net-tick' = {
  72. 'from' = { opacity = '0' }
  73. to = { opacity = '0' }
  74. }
  75. // ---- typed headings (tracker.worldapi.org#20): "Movie | Riddick" — `h1 { class = "typed" typeWord { class = "type-word
  76. // type-movie" … } typeSep { "|" } typeName { … } }`; the word in its type's colour, a muted bar, the name white. A flex row
  77. // (the server's markup has whitespace between the parts, a client-built page none — the gap makes both look the same).
  78. // MOBILE FIRST: a long name wraps inside its own column, right of "Show |", never sideways.
  79. '.type-movie' { color = colorMovie }
  80. '.type-series' { color = colorSeries }
  81. '.type-show' { color = colorShow }
  82. '.type-genre' { color = colorGenre }
  83. '.type-person' { color = colorArtist }
  84. '.type-franchise' { color = colorFranchise }
  85. '.type-timeline' { color = colorTimeline }
  86. 'h1.typed' { display = 'flex' alignItems = 'baseline' columnGap = '.4em' }
  87. typeWord { flex = 'none' fontWeight = '600' }
  88. typeSep { flex = 'none' fontWeight = '300' color = colorTextMuted }
  89. typeName { minWidth = '0' color = colorTextStrong overflowWrap = 'anywhere' }
  90. // ---- /shows/:slug (tracker.worldapi.org#4) --------------------------------------------------
  91. showPage { display = 'block' }
  92. // phone (mission 047, the creator): the title (+ Follow) first, then the poster at the full content width, then genres,
  93. // plot, cast — `showMeta` dissolves (display contents) so its title row can go before the poster (order -1). From 40rem
  94. // the creator's desktop layout: poster left (10rem), right of it the title and the rest.
  95. showHeader { display = 'flex' flexDirection = 'column' gap = '.6rem' marginBottom = '1.2rem' }
  96. showPoster { display = 'block' width = '100%' }
  97. 'show-poster img' { display = 'block' width = '100%' height = 'auto' borderRadius = '.4rem' aspectRatio = '2 / 3' objectFit = 'cover' background = colorSurface }
  98. // mission 060 (tracker.worldapi.org#27): the short id under the poster (and the person's photo) — small, muted, monospace; one
  99. // click selects all of it
  100. shortIdLabel { display = 'block' marginTop = '.3rem' font = '400 .8rem/1.2 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace' letterSpacing = '.05em' color = colorTextMuted textAlign = 'center' userSelect = 'all' }
  101. 'person-meta short-id-label' { textAlign = 'left' }
  102. showMeta { display = 'contents' overflowWrap = 'anywhere' }
  103. showTitle { order = '-1' display = 'flex' alignItems = 'center' gap = '.6rem' }
  104. 'show-title h1' { flex = '1' minWidth = '0' margin = '0' }
  105. 'button.follow' { flex = 'none' padding = '.3rem .8rem' fontSize = '.9rem' }
  106. 'button.follow.inverted' { padding = 'calc(.3rem - 1px) calc(.8rem - 1px)' }
  107. // tracker.worldapi.org#12: the show on TMDB / IMDb / TheTVDB / TVmaze — small monochrome badges (CSS only, no copied
  108. // logos), muted like the other secondary text, the accent on hover. Phone: right under the title row (order -1, after it
  109. // in the markup), above the full-width poster.
  110. showLinks { order = '-1' display = 'flex' flexWrap = 'wrap' gap = '.4rem' }
  111. '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 }
  112. 'a.extlink:hover' { textDecoration = 'none' background = colorAccent }
  113. // mission 058 (#24): a movie's watched check + its word; phone: under the title row (order -1), above the poster
  114. movieWatch { order = '-1' display = 'flex' alignItems = 'center' gap = '.4rem' }
  115. movieWatchLabel { fontSize = '.95rem' color = colorText }
  116. showGenres { display = 'flex' flexWrap = 'wrap' gap = '.4rem' margin = '.5rem 0' }
  117. '.genre-pill' { display = 'inline-block' padding = '.15rem .6rem' borderRadius = '1rem' fontSize = '.85rem' color = colorGenre border = '1px solid ' + colorGenre }
  118. showSummary { display = 'block' margin = '.5rem 0' fontSize = '.95rem' color = colorText }
  119. // tracker.worldapi.org#28: crew (one small muted label per job, the names after it) and the whole cast — "Name (Character)",
  120. // comma-separated as before; names past the first 20 (a crew job: 6) are hidden while the block is `collapsed`, the "Show all"
  121. // button (no border: not inverted) flips the block's class (client-side). The character and the comma are the link's ::after
  122. // (`data-role`): one element per name, so no SSR line break can put a space before the comma.
  123. showCrew { display = 'block' margin = '.5rem 0' }
  124. showCast { display = 'block' margin = '.5rem 0' }
  125. castHeading { display = 'block' fontSize = '.75rem' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }
  126. crewLabel { display = 'block' marginTop = '.35rem' fontSize = '.75rem' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }
  127. 'show-crew crew-label:first-child' { marginTop = '0' }
  128. castLabel { display = 'block' margin = '.4rem 0 .1rem' fontSize = '.75rem' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }
  129. '.artist-link' { color = colorArtist }
  130. '.crew-link' { color = colorArtist }
  131. 'a.artist-link::after' { content = "attr(data-role) ', '" color = colorTextMuted }
  132. 'a.crew-link::after' { content = "', '" color = colorTextMuted }
  133. 'a.artist-link.end::after,
  134. show-cast.collapsed a.artist-link.cut::after' { content = 'attr(data-role)' }
  135. 'a.crew-link.end::after,
  136. show-crew.collapsed a.crew-link.cut::after' { content = 'none' }
  137. 'show-cast.collapsed .more,
  138. show-crew.collapsed .more' { display = 'none' }
  139. 'button.showall' { display = 'block' margin = '.3rem 0 0' padding = '.15rem 0' fontSize = '.85rem' color = colorAccent background = 'transparent' }
  140. 'button.showall:hover' { background = 'transparent' textDecoration = 'underline' }
  141. 'show-cast.collapsed when-all,
  142. show-crew.collapsed when-all,
  143. show-cast.all when-collapsed,
  144. show-crew.all when-collapsed' { display = 'none' }
  145. showSeasons { display = 'block' }
  146. seasonRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.6rem 0' borderTop = '1px solid ' + colorBorder }
  147. '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' }
  148. seasonCount { fontWeight = '400' fontSize = '.85rem' color = colorTextMuted }
  149. // the caret at the header's right edge (mission 047): DOWN while closed, UP while open (tracker#17 — was right/down)
  150. seasonCaret { flex = 'none' alignSelf = 'center' display = 'grid' marginLeft = 'auto' color = colorTextMuted transition = 'transform .15s' }
  151. 'season-caret svg' { display = 'block' fill = 'none' stroke = 'currentColor' strokeWidth = '2' strokeLinecap = 'round' strokeLinejoin = 'round' }
  152. 'season-row.expanded season-caret' { transform = 'rotate(180deg)' }
  153. // episodes are NOT indented (mission 047): an episode's check sits under its season's check
  154. episodeRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.3rem 0' }
  155. episodeNumber { flex = 'none' width = '1.6rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  156. episodeTitle { flex = '1' minWidth = '0' overflowWrap = 'anywhere' }
  157. // mission 058 (#22): the episode's air date at the row's right edge, muted like the lists' dates
  158. episodeDate { flex = 'none' fontSize = '.85rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  159. // tracker#17: a season's placeholder rows while its episodes load — the episode row's shape (check disc, number, title) in
  160. // a moving gradient (the shimmer); same height as an episode row so nothing jumps when the real rows replace them
  161. skeletonRow { display = 'flex' alignItems = 'center' gap = '.6rem' padding = '.3rem 0' }
  162. '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' }
  163. skeletonCheck { flex = 'none' width = '2rem' height = '2rem' borderRadius = '50%' }
  164. skeletonNumber { flex = 'none' width = '1.6rem' height = '1rem' }
  165. skeletonTitle { flex = '1' maxWidth = '60%' height = '1rem' }
  166. 'skeleton-row:nth-of-type(even) skeleton-title' { maxWidth = '42%' }
  167. // tracker#26: an incomplete title while its details load (components/show.hl) — the poster's place (2:3, the poster's
  168. // width), a cast line, and season-shaped rows; the same shimmer
  169. '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' }
  170. skeletonPoster { width = '100%' aspectRatio = '2 / 3' }
  171. skeletonCast { width = '70%' height = '1rem' margin = '.5rem 0' }
  172. showLoading { display = 'block' marginTop = '1rem' }
  173. // its tick (like personProbe): rendered, invisible
  174. showProbe { position = 'fixed' left = '0' top = '0' width = '1px' height = '1px' opacity = '0' pointerEvents = 'none' animation = 'tracker-person-tick .1s linear infinite' }
  175. '@keyframes tracker-shimmer' = {
  176. 'from' = { backgroundPosition = '100% 0' }
  177. to = { backgroundPosition = '0% 0' }
  178. }
  179. // the check: "a disc with a check in it" (drawn in CSS/SVG, no icon font) — solid (filled accent disc, dark check)
  180. // once watched; while not, its INVERSION (mission 047, the creator): an outline circle AND an outline check. tracker#29
  181. // (the creator: "in /my/unwatched all checkboxes are active"): in the accent colour the outline read as ticked, so the
  182. // unwatched outline is MUTED grey (empty, nothing filled) and only turns accent under the pointer; watched stays solid accent
  183. 'button.checkicon' { flex = 'none' display = 'grid' placeItems = 'center' width = '2rem' height = '2rem' padding = '0' background = 'transparent' border = '0' borderRadius = '50%' cursor = 'pointer' }
  184. '.checkicon circle' { fill = 'transparent' stroke = colorTextMuted strokeWidth = '1.5' }
  185. '.checkicon path' { fill = 'none' stroke = colorTextMuted strokeWidth = '2' strokeLinecap = 'round' strokeLinejoin = 'round' }
  186. 'button.checkicon.regular:hover circle,
  187. button.checkicon.regular:hover path' { stroke = colorAccent }
  188. '.checkicon.solid circle' { fill = colorAccent stroke = colorAccent }
  189. '.checkicon.solid path' { stroke = colorAccentText }
  190. // ---- /unwatched (tracker.worldapi.org#5) ----------------------------------------------------
  191. unwatchedPage { display = 'block' }
  192. unwatchedRows { display = 'block' }
  193. // phone: two lines next to the check — show title + date, below it the episode; one line from 40rem
  194. unwatchedRow { display = 'grid' gridTemplateColumns = '2rem minmax(0, 1fr) auto' alignItems = 'center' gap = '0 .6rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }
  195. 'unwatched-row button.checkicon' { gridRow = 'span 2' }
  196. '.unwatched-show' { fontWeight = '600' overflowWrap = 'anywhere' }
  197. unwatchedEpisode { gridColumn = '2 / 4' gridRow = '2' color = colorText overflowWrap = 'anywhere' }
  198. unwatchedDate { gridRow = '1' gridColumn = '3' fontSize = '.85rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  199. // ---- /schedule (tracker.worldapi.org#6) — same row layout as /unwatched, minus the check icon -----
  200. schedulePage { display = 'block' }
  201. scheduleRows { display = 'block' }
  202. scheduleRow { display = 'grid' gridTemplateColumns = 'minmax(0, 1fr) auto' alignItems = 'center' gap = '0 .6rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }
  203. '.schedule-show' { fontWeight = '600' overflowWrap = 'anywhere' }
  204. scheduleEpisode { gridColumn = '1 / 3' gridRow = '2' color = colorText overflowWrap = 'anywhere' }
  205. scheduleDate { gridRow = '1' gridColumn = '2' fontSize = '.85rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  206. // ---- /my/shows (tracker.worldapi.org#7) — small poster, title, last watched episode ------------
  207. myShowsPage { display = 'block' }
  208. myShowsRows { display = 'block' }
  209. myShowRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.5rem 0' borderTop = '1px solid ' + colorBorder }
  210. 'img.myshow-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }
  211. '.myshow-title' { flex = '1' minWidth = '0' fontWeight = '600' overflowWrap = 'anywhere' }
  212. myShowEpisode { flex = 'none' fontSize = '.9rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  213. // /my/movies (mission 058, #24): a watched movie's check, the episodes' solid disc, not clickable
  214. myMovieWatched { flex = 'none' display = 'grid' placeItems = 'center' width = '2rem' height = '2rem' }
  215. // ---- franchises and timelines (tracker.worldapi.org#19, mission 066) ------------------------------------------------------
  216. // the widget on a show / movie page: the franchise above, then ONE row "‹ Prequel | Timeline | Sequel ›" — three columns that
  217. // never scroll sideways on a phone (the outer two shrink, their title cut to one line); a disabled end is muted
  218. franchiseWidgets { display = 'block' margin = '0 0 1.2rem' }
  219. franchiseWidget { display = 'block' padding = '.5rem .6rem' marginBottom = '.5rem' background = colorSurface borderRadius = '.5rem' }
  220. 'a.fw-franchise' { display = 'block' marginBottom = '.3rem' fontSize = '.8rem' fontWeight = '600' letterSpacing = '.04em' textTransform = 'uppercase' textAlign = 'center' }
  221. fwRow { display = 'grid' gridTemplateColumns = 'minmax(0, 1fr) minmax(0, 1.2fr) minmax(0, 1fr)' alignItems = 'center' gap = '.5rem' }
  222. '.fw-side' { display = 'flex' flexDirection = 'column' minWidth = '0' color = colorAccent }
  223. '.fw-next' { alignItems = 'flex-end' textAlign = 'right' }
  224. '.fw-side.off' { color = colorTextMuted opacity = '.5' }
  225. 'a.fw-side:hover' { textDecoration = 'none' }
  226. 'a.fw-side:hover fw-dir' { textDecoration = 'underline' }
  227. fwDir { fontWeight = '600' whiteSpace = 'nowrap' }
  228. fwTitle { fontSize = '.8rem' color = colorTextMuted whiteSpace = 'nowrap' overflow = 'hidden' textOverflow = 'ellipsis' maxWidth = '100%' }
  229. 'a.fw-timeline' { minWidth = '0' fontWeight = '600' color = colorTextStrong textAlign = 'center' overflowWrap = 'anywhere' padding = '0 .4rem' borderLeft = '1px solid ' + colorBorder borderRight = '1px solid ' + colorBorder }
  230. // /franchises, /franchises/<slug>, /timelines/<slug> and the editor
  231. franchisesPage { display = 'block' }
  232. franchisePage { display = 'block' }
  233. franchisesHead { display = 'flex' alignItems = 'center' gap = '.6rem' marginBottom = '.8rem' }
  234. 'franchises-head h1' { flex = '1' minWidth = '0' margin = '0' overflowWrap = 'anywhere' }
  235. 'franchises-head a.button' { flex = 'none' padding = '.3rem .8rem' fontSize = '.9rem' }
  236. 'a.franchise-up' { display = 'inline-block' marginBottom = '.3rem' fontSize = '.85rem' fontWeight = '600' letterSpacing = '.04em' textTransform = 'uppercase' }
  237. 'p.franchise-summary' { margin = '-.3rem 0 1rem' color = colorText }
  238. franchiseList { display = 'block' marginBottom = '1.2rem' }
  239. 'a.franchise-row' { display = 'flex' alignItems = 'baseline' gap = '.8rem' padding = '.6rem 0' color = colorTextStrong borderTop = '1px solid ' + colorBorder }
  240. franchiseName { flex = '1' minWidth = '0' fontSize = '1.05rem' fontWeight = '600' overflowWrap = 'anywhere' }
  241. franchiseCount { flex = 'none' fontSize = '.85rem' color = colorTextMuted }
  242. timelineList { display = 'grid' gridTemplateColumns = 'minmax(0, 1fr)' gap = '0 1.2rem' }
  243. 'a.timeline-link' { display = 'flex' alignItems = 'baseline' gap = '.6rem' padding = '.35rem 0' color = colorText borderTop = '1px solid ' + colorBorder }
  244. timelineLinkName { flex = '1' minWidth = '0' overflowWrap = 'anywhere' }
  245. timelineLinkCount { flex = 'none' fontSize = '.8rem' color = colorTextMuted }
  246. timelineBlocks { display = 'block' }
  247. timelineHead { display = 'flex' flexWrap = 'wrap' alignItems = 'center' gap = '.4rem .8rem' padding = '.8rem 0 .4rem' marginTop = '.6rem' borderBottom = '2px solid ' + colorBorder }
  248. timelineTitle { flex = '1' minWidth = '0' display = 'flex' alignItems = 'baseline' gap = '.6rem' }
  249. 'a.timeline-name' { fontSize = '1.05rem' fontWeight = '600' color = colorTextStrong overflowWrap = 'anywhere' }
  250. timelineCount { flex = 'none' fontSize = '.85rem' color = colorTextMuted }
  251. sortToggle { flex = 'none' display = 'flex' padding = '.15rem' background = colorSurface borderRadius = '.45rem' }
  252. 'button.sortbtn' { padding = '.25rem .7rem' fontSize = '.85rem' color = colorText background = 'transparent' }
  253. 'button.sortbtn.on' { color = colorAccentText background = colorAccent }
  254. 'a.timeline-item' { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.45rem 0' color = colorText borderTop = '1px solid ' + colorBorder }
  255. 'a.timeline-item:hover' { textDecoration = 'none' }
  256. 'a.timeline-item:hover film-title' { color = colorAccent }
  257. franchiseEditPage { display = 'block' }
  258. timelineEditPage { display = 'block' }
  259. editSection { display = 'flex' flexDirection = 'column' gap = '.5rem' marginBottom = '1.4rem' }
  260. 'edit-section input, edit-section textarea' { display = 'block' width = '100%' padding = '.55rem .75rem' font = 'inherit' color = colorTextStrong background = colorSurface border = '0' borderRadius = '.4rem' outline = 'none' }
  261. 'edit-section input:focus, edit-section textarea:focus' { boxShadow = '0 0 0 2px ' + colorAccent }
  262. 'edit-section > button' { alignSelf = 'flex-start' }
  263. chipRow { display = 'flex' flexWrap = 'wrap' gap = '.4rem' }
  264. 'button.chip' { padding = '.3rem .8rem' fontSize = '.9rem' color = colorText background = colorSurface }
  265. 'button.chip.on' { color = colorAccentText background = colorAccent }
  266. 'a.edit-new-franchise' { fontSize = '.9rem' }
  267. editRows { display = 'block' }
  268. editRow { display = 'flex' alignItems = 'center' gap = '.5rem' padding = '.4rem 0' borderTop = '1px solid ' + colorBorder }
  269. editPos { flex = 'none' width = '1.6rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' textAlign = 'right' }
  270. 'button.mv' { flex = 'none' width = '2.4rem' height = '2.4rem' padding = '0' fontSize = '1.1rem' }
  271. 'button.mv.off' { opacity = '.3' pointerEvents = 'none' }
  272. 'button.mv.rm' { color = colorDanger }
  273. // ---- / (tracker.worldapi.org#13): signed in, four big icon tiles; then the text and two poster rows ----------------
  274. home { display = 'block' }
  275. // tracker#21: five tiles (Unwatched, Schedule, Series, Shows, Movies) — three per row on a phone, one row of five from 40rem
  276. homeNav { display = 'grid' gridTemplateColumns = 'repeat(3, minmax(0, 1fr))' gap = '.6rem' marginBottom = '1.2rem' }
  277. 'a.hometile' { display = 'flex' flexDirection = 'column' alignItems = 'center' justifyContent = 'center' gap = '.4rem' padding = '1rem .5rem' borderRadius = '.6rem' color = colorTextStrong background = colorSurface fontWeight = '600' }
  278. 'a.hometile:hover' { textDecoration = 'none' color = colorAccent }
  279. 'a.hometile svg' { display = 'block' width = '2.5rem' height = '2.5rem' fill = 'none' stroke = colorAccent strokeWidth = '1.6' strokeLinecap = 'round' strokeLinejoin = 'round' }
  280. homeIntro { display = 'block' marginBottom = '1rem' color = colorText }
  281. 'home-intro p' { margin = '0 0 .6rem' }
  282. homeSection { display = 'block' marginBottom = '1.2rem' }
  283. 'home-section h2' { margin = '0 0 .5rem' fontSize = '1.1rem' }
  284. 'home-section h2 a' { color = colorTextStrong }
  285. // tracker#20: Movies / Series / Shows in their type's colour (more specific than the rule above)
  286. 'home-section h2 a.type-movie' { color = colorMovie }
  287. 'home-section h2 a.type-series' { color = colorSeries }
  288. 'home-section h2 a.type-show' { color = colorShow }
  289. 'home-section h2 a::after' { content = "' ›'" color = colorAccent }
  290. // phone: the row scrolls sideways INSIDE itself (the page never does); from 40rem a grid of 5
  291. tileRow { display = 'flex' gap = '.6rem' overflowX = 'auto' scrollSnapType = 'x proximity' paddingBottom = '.4rem' }
  292. 'tile-row a.tile' { flex = 'none' width = '7rem' scrollSnapAlign = 'start' }
  293. // /movies, /series and /shows: a grid of 3 on a phone, 6 from 40rem
  294. listPage { display = 'block' }
  295. '.list-count' { margin = '-.4rem 0 .8rem' fontSize = '.9rem' }
  296. tileGrid { display = 'grid' gridTemplateColumns = 'repeat(3, minmax(0, 1fr))' gap = '.8rem .6rem' marginBottom = '1rem' }
  297. pagerBox { display = 'block' margin = '1rem 0' }
  298. // the copied pagination (components/pagination.hl, its own Style) a bit tighter on a phone, so "Previous 1 … 4 5 6 … 326
  299. // Next" stays one row at 390 px; from 40rem its own sizes
  300. 'pager-box pagination ol' { gap = '.1rem' }
  301. 'pager-box pagination button' { minWidth = '1.8rem' padding = '.3rem .3rem' fontSize = '.95rem' }
  302. // a tile: the poster (2:3, the placeholder until the sync has a real one), the title (two lines at most), the year
  303. 'a.tile' { display = 'block' minWidth = '0' color = colorText }
  304. 'a.tile:hover' { textDecoration = 'none' }
  305. 'a.tile:hover tile-title' { color = colorAccent }
  306. 'img.tile-poster' { display = 'block' width = '100%' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.35rem' background = colorSurface }
  307. 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' }
  308. tileYear { display = 'block' fontSize = '.8rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  309. myMoviesPage { display = 'block' }
  310. // ---- the search (tracker.worldapi.org#14) — the header's magnifier and /search ------------------------------------
  311. 'a.searchlink' { flex = 'none' display = 'grid' placeItems = 'center' width = '2rem' height = '2rem' color = colorText borderRadius = '.4rem' }
  312. 'a.searchlink:hover' { color = colorAccent background = colorSurface }
  313. 'a.searchlink svg' { display = 'block' fill = 'none' stroke = 'currentColor' strokeWidth = '2' strokeLinecap = 'round' }
  314. searchPage { display = 'block' }
  315. searchBar { display = 'block' marginBottom = '.8rem' }
  316. '#searchinput' { display = 'block' width = '100%' padding = '.55rem .75rem' font = 'inherit' color = colorTextStrong background = colorSurface border = '0' borderRadius = '.4rem' outline = 'none' }
  317. '#searchinput:focus' { boxShadow = '0 0 0 2px ' + colorAccent }
  318. searchSection { display = 'block' marginBottom = '1rem' }
  319. 'search-section h2' { margin = '.4rem 0 .2rem' fontSize = '.85rem' fontWeight = '600' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }
  320. searchRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.4rem 0' borderTop = '1px solid ' + colorBorder }
  321. webRow { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.4rem 0' borderTop = '1px solid ' + colorBorder }
  322. 'img.search-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }
  323. '.search-title' { flex = '1' minWidth = '0' fontWeight = '600' overflowWrap = 'anywhere' }
  324. '.search-person' { flex = '1' minWidth = '0' color = colorArtist overflowWrap = 'anywhere' }
  325. searchType { flex = 'none' fontSize = '.8rem' color = colorTextMuted }
  326. webText { flex = '1' minWidth = '0' display = 'flex' flexDirection = 'column' }
  327. webTitle { fontWeight = '600' color = colorTextStrong overflowWrap = 'anywhere' }
  328. 'button.add' { flex = 'none' padding = 'calc(.3rem - 1px) calc(.8rem - 1px)' fontSize = '.9rem' }
  329. webBar { display = 'flex' alignItems = 'center' gap = '.8rem' margin = '.6rem 0' }
  330. // ---- /person/<slug> (tracker.worldapi.org#16): photo + facts + bio, then the filmography rows ----------------------
  331. // the photo left of the name (phone 6rem, from 40rem 10rem). Rows like the search's: small poster, title, year · kind,
  332. // the role below
  333. personPage { display = 'block' }
  334. personHeader { display = 'flex' alignItems = 'flex-start' gap = '.8rem' marginBottom = '1rem' }
  335. personPhoto { flex = 'none' display = 'block' width = '6rem' }
  336. 'person-photo img' { display = 'block' width = '100%' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.4rem' background = colorSurface }
  337. personMeta { display = 'block' minWidth = '0' overflowWrap = 'anywhere' }
  338. personFact { display = 'block' fontSize = '.95rem' color = colorTextMuted }
  339. // the bio under photo + facts, full width; four lines until clicked (components/person.hl toggleBio)
  340. personBio { display = 'block' margin = '0 0 1rem' fontSize = '.95rem' whiteSpace = 'pre-line' cursor = 'pointer' }
  341. 'person-bio.short' { display = '-webkit-box' overflow = 'hidden' '-webkit-line-clamp' = '4' '-webkit-box-orient' = 'vertical' }
  342. 'h2.film-heading' { margin = '.4rem 0 .2rem' fontSize = '.85rem' fontWeight = '600' letterSpacing = '.04em' textTransform = 'uppercase' color = colorTextMuted }
  343. personLoading { display = 'block' padding = '.6rem 0' color = colorTextMuted animation = 'tracker-person-pulse 1.2s ease-in-out infinite alternate' }
  344. // the fill's tick (components/person.hl): rendered (an animation needs it) but invisible, like netProbe
  345. personProbe { position = 'fixed' left = '0' top = '0' width = '1px' height = '1px' opacity = '0' pointerEvents = 'none' animation = 'tracker-person-tick .1s linear infinite' }
  346. '@keyframes tracker-person-tick' = {
  347. 'from' = { opacity = '0' }
  348. to = { opacity = '0' }
  349. }
  350. '@keyframes tracker-person-pulse' = {
  351. 'from' = { opacity = '1' }
  352. to = { opacity = '.45' }
  353. }
  354. filmRows { display = 'block' }
  355. 'a.film-row' { display = 'flex' alignItems = 'center' gap = '.8rem' padding = '.45rem 0' color = colorText borderTop = '1px solid ' + colorBorder }
  356. 'a.film-row:hover' { textDecoration = 'none' }
  357. 'a.film-row:hover film-title' { color = colorAccent }
  358. 'img.film-poster' { flex = 'none' display = 'block' width = '2.5rem' height = 'auto' aspectRatio = '2 / 3' objectFit = 'cover' borderRadius = '.25rem' background = colorSurface }
  359. filmText { flex = '1' minWidth = '0' display = 'flex' flexDirection = 'column' }
  360. filmTitle { fontWeight = '600' color = colorTextStrong overflowWrap = 'anywhere' }
  361. filmMeta { display = 'flex' gap = '.5rem' fontSize = '.8rem' color = colorTextMuted fontVariantNumeric = 'tabular-nums' }
  362. filmRole { fontSize = '.85rem' color = colorArtist overflowWrap = 'anywhere' }
  363. 'film-role:empty' { display = 'none' }
  364. 'film-year:empty' { display = 'none' }
  365. // ---- wider screens (≥ 40rem = 640 px): only what changes from the phone -------------------------
  366. '@media (min-width: 40rem)' = {
  367. applicationHeader = { gap = '1rem' padding = '.6rem 1rem' }
  368. userBox = { gap = '.5rem' }
  369. '#logout' = { padding = '.4rem .9rem' fontSize = '1rem' }
  370. identSelector = { '--ident-font' = '600 .9rem/1.4 system-ui, sans-serif' }
  371. 'ident-selector::part(button)' = { padding = '.35rem .8rem' }
  372. 'ident-selector::part(status)' = { padding = '.35rem .8rem' }
  373. '#loginbutton' = { padding = '.35rem .8rem' fontSize = '1rem' }
  374. '#loginbutton .wide' = { display = 'inline' }
  375. main = { padding = '1rem' }
  376. h1 = { margin = '0 0 1rem' fontSize = '1.3rem' }
  377. showHeader = { flexDirection = 'row' alignItems = 'flex-start' gap = '1rem' marginBottom = '1.5rem' }
  378. showPoster = { flex = 'none' width = '10rem' }
  379. showMeta = { display = 'block' flex = '1' minWidth = '0' }
  380. showTitle = { marginBottom = '1rem' }
  381. movieWatch = { margin = '-.4rem 0 .8rem' }
  382. showSummary = { fontSize = '1rem' }
  383. episodeRow = { padding = '.4rem 0' }
  384. skeletonRow = { padding = '.4rem 0' }
  385. unwatchedRow = { display = 'flex' }
  386. '.unwatched-show' = { flex = 'none' }
  387. unwatchedEpisode = { flex = '1' }
  388. unwatchedDate = { flex = 'none' }
  389. scheduleRow = { display = 'flex' }
  390. '.schedule-show' = { flex = 'none' }
  391. scheduleEpisode = { flex = '1' }
  392. scheduleDate = { flex = 'none' }
  393. homeNav = { gridTemplateColumns = 'repeat(5, minmax(0, 1fr))' gap = '1rem' }
  394. 'a.hometile' = { padding = '1.4rem .5rem' }
  395. tileRow = { display = 'grid' gridTemplateColumns = 'repeat(5, minmax(0, 1fr))' gap = '1rem .8rem' overflowX = 'visible' }
  396. 'tile-row a.tile' = { width = 'auto' }
  397. tileGrid = { gridTemplateColumns = 'repeat(6, minmax(0, 1fr))' gap = '1rem .8rem' }
  398. 'pager-box pagination ol' = { gap = '.3rem' }
  399. 'pager-box pagination button' = { minWidth = '2.2rem' padding = '.35rem .6rem' fontSize = '1rem' }
  400. personHeader = { gap = '1rem' }
  401. personPhoto = { width = '10rem' }
  402. timelineList = { gridTemplateColumns = 'repeat(2, minmax(0, 1fr))' }
  403. fwRow = { gridTemplateColumns = 'minmax(0, 1fr) minmax(0, 1.5fr) minmax(0, 1fr)' }
  404. }

Branches

Latest commits

  • 749019b0Merge t19 (tracker#19 franchises + timelines) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs/faketmdb.mjs, both sides kept; franchise/timeline pages get #20's typed heading (Franchise | …, Timeline | …), their title links via titlePath (/movies|/series|/shows); the collection seed waits for repair, kinds and credits too; franchises.mjs URLs + 2 new checks; gates browser 323/0, kinds 32/0, franchises 50/0mre
  • daf49feaMerge t20 (tracker#20 typed headings + #21 series/shows split) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/search.hl/components/search.hl/browser.mjs/faketmdb.mjs, both sides kept; clock order backfill → repair → kinds → credits; withDetailsFields stores tmdbType + kind; gates: browser.mjs URLs → /series|/movies, typed h1 selectors; kinds.mjs repair/credits off, fixture name = seed name; browser 323/0, kinds 32/0mre
  • 20e09d89Merge t18 (tracker#18 delta sync) into main (mission 070): conflicts README/STATUS/show.hl/project.hl/browser.mjs, both sides kept; pageShowOf summary = summaryOfmre
  • f83571c3tracker#18 (mission 067): daily sync by change lists — TMDB /tv|movie/changes (since the stored day, paged) + TVmaze /updates/shows → only our changed titles (followed: full step, unfollowed: light step — changed seasons, no TVmaze), full walk on first run / gap > 14 days / failed list; show record refreshed (title, tmdbSummary, tagline, status, genres …; renamed titles re-indexed); summary = the creator's own text (page: summary > tmdbSummary > tvmazeSummary), one-time clear of copied summaries (9,647 on the live copy); gate 261, tests/realdata-018*.mjs, README + STATUSmre
  • 1ed5457etracker#20 + #21 (mission 068): typed headings "<Type> | <name>" in type colours; TV titles split into Series (/series) and Shows (/shows) by TMDB type + Reality/Talk/News genres — kind stored by sync/import/adult backfill + new kind backfill (resumes), /movies/<slug>, /shows/<slug> of a series/movie → 301, /my/series + /my/shows, home 5 tiles + 3 rows, search/filmography labels; gates kinds 32 + browser 266, tests/realdata-068.mjs, tools/count-kinds.hl, docs/kinds.md, README + STATUSmre
  • f2b00674Merge t26 (tracker#26 + #28) into main (mission 062): short ids for every new person (castPersonId, guest route), guest stars stored on the title and created as people only when opened (/person/tmdb/<id>?show=<id> → 302), lean watch/follow clicks (showRow a small object, cast/crew from the slug, watches cached per user, face rows only after a season toggle); gate 311, tests/realdata-062.mjs, README + STATUSmre
  • 0553b51ftracker#19 (mission 066): franchises and timelines — tables, /franchises, /franchises/<slug>, /timelines/<slug> (Timeline | Release sort, series by last episode), the Prequel | Timeline | Sequel widget with the franchise above, the creator's editor, TMDB collection seed in the app (resumes, paced); gate tests/franchises.mjs 48/0 + browser.mjs 266/0, tests/realdata-066.mjs, docs/franchises.md, README + STATUSmre
  • fa1f9dfatracker#29 (mission 063): unwatched check muted grey outline + check (accent only on hover), watched stays solid — no code regression, the accent outline read as ticked; gate checks real checks visibly (computed style + screenshot pixel) on /my/unwatched, show, movie, /my/movies; gate 266, tests/realdata-063.mjs, README + STATUSmre
  • 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
  • d8b12d67tracker#27 (mission 060): short ids for movies, series and persons — old 702 kept (data/old-short-ids.json), new random [a-z0-9]{5} unique across both, claimed at creation, background backfill (resumes), shown under poster/photo, /<shortId> → 301; gate 259, tests/realdata-060*, 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
  • f14db671tracker#22-#25 (mission 058): episode air dates, season check = all episodes watched, movie watched check (+ /my/movies count), /genres/<genre> pages (movies + series, newest first, paginated); gate 238, tests/realdata-058.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