gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit8751adb88751adb8tracker: report 032mre8751adb8/components/styles.hl

36.0 KB

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

Branches

Latest commits

  • 8751adb8tracker: report 032mre
  • 9bce1f65tracker mission 032: STATUS gate files + the hour-boundary flakemre
  • 718bfb89tracker#37 (mission 032): /people = everyone, last updated first (updatedAt stamped by the person fill; view built at boot, touched people first at once), photo + name tiles (person colour) with the /movies pagination, /people/<letter> removed; photo = our file, tmdbProfile, a cast/crew entry's profile (in-memory map at boot), else the new 'no photo' placeholder; new cast/crew/created_by people keep tmdbProfile; search people rows with the photo; /settings = the heading only; util.hl sortDesc starts from sorted runs (same result, 105k: 1.6 s -> 0.15 s); gates 369/0, 32/0, 52/0, check-theme 0; README/STATUS/LOGmre
  • 96ba683adeploy.sh: a gate without a 'passed,' line (check-theme) no longer ends the scriptmre
  • eb3b9205tracker: report 031mre
  • 9b5d2e89tracker mission 031: README (What it does, Test: four gates + the #32 checks, Files: theme/, new pages), STATUS (real copy, A/B load, how to repeat, open points), LOGmre
  • 39950e4ctracker#32 (mission 031): the WorldAPI theme (theme/ vendored verbatim from layouts.worldapi.org 85b5654; styles.hl inherits it: accent green-dark, type colours 1-6; own base/header rules, row lines, genre-pill and inverted-button frames removed, the season foldable keeps its line; check-theme 21 -> 0, 4th deploy gate; main actions class primary) and the #32 header (theme AppHeader/MainMenu/UserMenu/Sidebar/ContentFirst: desktop brand, search, Series|Shows|Movies|Genres|People, user icon with Unwatched..Settings, Logout; signed out the ident selector, phone the iD icon dropdown; phone menu in the sidebar overlay; marked entry by :has); /find -> /search/<q>, /genres, /people(/<letter>), /settings; main { ContentFirst { slot } } works around the hl:web one-line slot bug; gates 365/0, 32/0, 52/0, check-theme 0mre
  • a386dc92tracker: reports 029 + 030mre
  • 71e0fd7dtracker missions 029 + 030: README (What it does, Files, gate count), STATUS (real-copy numbers, how to repeat, open points), LOGmre
  • d36ea6eatracker#34 + #35 (mission 030): Follow directly under the poster, as wide as the poster (show.hl, styles.hl); the status pill next to a series' title — TVmaze's status (new tvmazeStatus, stored by the sync's TVmaze merge) else TMDB's, TVmaze Ended + TMDB Canceled = Canceled, inverted (filled, dark text, no border), green running / yellow pending / red canceled / muted ended (shows.hl statusOf); the daily delta asks TVmaze's status of an unfollowed series TVmaze's change list names (dailysync.hl syncRunStep, sync.hl syncTvmazeStatus); the status backfill after the details repair (backfill.hl, jobs.hl statusTick; resumable, 550 ms per TVmaze request); gates 354/0, 32/0, 52/0mre
  • 7d7d4487tracker#33 (mission 029): reduced titles — every title TMDB's details never went through this app (no detailsAt, no tmdbSync) is incomplete (shows.hl isIncomplete; the old tracker's migrated rows passed #26's test: 5,697 non-adult on the live copy, 691 series without seasons); the repair job does the visibly reduced first (shows.hl missingParts), the page completes one on open; a title TMDB has no poster for (The Remaining) shows the placeholder; tools/count-incomplete.hl; gate fixtures stand for synced titles (tmdbSync), tests/seed-reduced.hl + #33 checks; gates 347/0, 32/0, 52/0mre
  • 661c2592tracker: report 028mre
  • 27c916fatracker mission 028: README ("Code order", the new file map), STATUS (counts before/after, tests, how to repeat, open), LOGmre
  • d924f398tracker mission 028: comments name the new files (sync.hl, dailysync.hl, backfill.hl, credits.hl, jobs.hl, images.hl …); tools/ref-params.py + tools/lambda-audit.py also scan lib/ (they globbed the root only), lambda-audit counts a plain `x = p` alias like `let x = p`mre
  • 2e89b968tracker mission 028 (code order) 5/5 let: `let` only where a variable is reassigned — 667 never-reassigned lets became plain declarations (project.hl, lib/, components/, tools/, tests/); kept: 264 in loop bodies (a plain declaration there is 'Cannot reassign' on the 2nd pass), 234 reassigned, 27 whose name is also a member/outer/free name (a plain write would rebind it); tools/let-audit.py decides and fixes (README 'Code order'); tests/realdata-m028.{sh,mjs} = the page-output diff on a real copy; gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 54796ff2tracker mission 028 (code order) 4/5 thin faces + last copies: the show page's check/follow faces call lib/watches.hl toggleWatched / toggleSeasonWatched (seasonAllWatched moved there) and lib/follows.hl toggleFollowed; both logins (header selector face, /login/callback) share lib/users.hl userOfCode; todayStr/listOf copies in components and the export readers copied into tools/migrate.hl + tools/old-short-ids.hl now once (lib/util.hl, lib/export.hl); gates 342/0, 32/0, 52/0; old-short-ids output byte-identical, migrate output identicalmre
  • 06b078e3tracker mission 028 (code order) 3/5 project.hl is the map: config, routes, wiring and a feature → file index (914 → 258 lines); the background jobs (daily sync run, backfills, details repair, credits job, merge, short ids, collection seed) moved unchanged into lib/jobs.hl (a class: their state is reassigned every step, a static cannot be; one instance made after the server), the login callback into lib/users.hl, poster/photo serving into lib/images.hl, the /shows/<slug> rule into lib/shows.hl showsMovedPath; route handlers are thin wrappers; gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 94716fd2tracker mission 028 (code order) 2/5 util + topics: lib/util.hl holds envOr, storageDir, postersDir, profilesDir, newId, hexDigits, todayStr, dateOr, textOr, hasId, listOr, firstOf, sortDesc once (were copied into up to 5 files); tmdbsync.hl split into tmdb.hl (TMDB/TVmaze requests), sync.hl (one title's sync), sync-helpers.hl, backfill.hl; details.hl split into details.hl, credits.hl, credits-helpers.hl (isIncomplete to shows.hl); search-helpers.hl (words, query, ranking, slugs); collections.hl (the TMDB collection seed, out of franchises.hl); deltasync.hl renamed dailysync.hl; no behaviour change: gates 342/0, 32/0, 52/0, real-copy pages identicalmre
  • 186079b0tracker mission 028 (code order) 1/5 move: every root .hl except project.hl into lib/ (styles.hl into components/), import paths only; gates 342/0, 32/0, 52/0; real-copy pages identicalmre
  • 4f47f181tracker: report 027mre