feat(web): movie detail page at /movies/{id}

This commit is contained in:
Miguel Palhas
2026-08-24 00:00:18 +01:00
parent 8af85606a0
commit fdaaeccb40
7 changed files with 1181 additions and 181 deletions
+297
View File
@@ -1284,6 +1284,274 @@ body {
overflow-wrap: anywhere;
}
/* ---- movie page (§9.6, issue #149) ------------------------------------- */
/* The hero is one machined module carrying what the film IS. The backdrop
hotlinks from image.tmdb.org (§9.6) and sinks into the console charcoal
under a scrim, so the readouts keep their contrast — a photo never sits
behind text bare. */
.movie-hero {
position: relative;
overflow: hidden;
margin: 0 0 var(--space-8);
}
/* the backdrop layer: painted only when detail supplied one */
.movie-hero::before {
content: "";
position: absolute;
inset: 0;
background-image: var(--backdrop);
background-size: cover;
background-position: center 20%;
opacity: 0;
transition: opacity 400ms var(--ease-out);
}
.movie-hero.has-backdrop::before {
opacity: 1;
}
/* the scrim: ground at the bottom, heavy everywhere the text sits */
.movie-hero::after {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(
to top,
oklch(from var(--ground) l c h / 92%) 0%,
oklch(from var(--ground) l c h / 72%) 45%,
oklch(from var(--ground) l c h / 40%) 100%
),
oklch(from var(--ground) l c h / 35%);
}
.movie-body {
position: relative;
z-index: 1;
display: flex;
gap: var(--space-6);
align-items: flex-start;
}
/* the poster is framed evidence, like every other image on a panel */
.movie-poster {
flex: none;
width: 10.5rem;
aspect-ratio: 2 / 3;
object-fit: cover;
border: 1px solid var(--line-strong);
border-radius: var(--radius);
box-shadow: 0 2px 12px oklch(from var(--lowlight) l c h / 75%);
}
.movie-info {
min-width: 0;
display: grid;
gap: var(--space-2);
padding: var(--space-6) 0;
}
.movie-idline {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
}
/* the page's voice: bigger than a deck title, same display face */
.movie-title {
margin: 0;
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: 700;
letter-spacing: 0.04em;
line-height: 1.05;
}
.movie-year {
font-size: var(--text-sm);
}
.movie-chips {
margin-left: 0;
}
/* rating and meta are readouts on one line each; the star is drawn */
.movie-rating {
display: inline-flex;
align-items: center;
gap: var(--space-1);
margin: 0;
color: var(--ink);
}
.star {
width: 0.75rem;
height: 0.75rem;
fill: var(--ink-muted);
}
.movie-meta {
margin: 0;
}
.movie-tagline {
margin: var(--space-1) 0 0;
font-family: var(--font-display);
font-size: var(--text-md);
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink-muted);
}
.movie-overview {
max-width: 65ch;
margin: var(--space-1) 0 0;
color: var(--ink);
}
.movie-actions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-2);
}
.movie-actions .add-note[data-tone="fault"] {
margin: 0;
color: var(--signal-fault);
}
/* external links wear the control skin; anchors must drop the underline */
.movie-actions a.control {
text-decoration: none;
}
/* library controls follow the hero on their own row: state left, errands right */
.movie-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2) var(--space-3);
padding: var(--space-2) var(--space-1);
}
.movie-controls .control {
min-height: 2.25rem;
padding: 0 var(--space-3);
}
/* wanted and blocked read as rules, pressed = on — the tracked-toggle idiom */
.movie-controls .control[aria-pressed="true"] {
color: var(--accent-bright);
border-color: var(--accent);
}
.movie-root-field {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.movie-root {
width: auto;
min-width: 11rem;
}
.movie-controls-space {
flex: 1;
}
/* cast: top billed, each linking out to TMDB (§9.6) */
.cast-grid {
margin: 0;
padding: var(--space-3) var(--space-1) 0;
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
gap: var(--space-4) var(--space-2);
}
.cast-link {
display: grid;
gap: var(--space-1);
justify-items: start;
color: inherit;
text-decoration: none;
min-width: 0;
}
.cast-link:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.cast-photo {
width: 100%;
aspect-ratio: 2 / 3;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel-raised);
transition: border-color 150ms var(--ease-out);
}
.cast-photo img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* no profile photo: dead air, not a broken frame */
.cast-photo-none::before {
content: "";
display: block;
width: 40%;
aspect-ratio: 1;
margin: 30% auto 0;
border-radius: 50%;
border: 1px solid var(--line);
}
.cast-link:hover .cast-photo,
.cast-link:focus-visible .cast-photo {
border-color: var(--accent);
}
.cast-name {
font-size: var(--text-xs);
color: var(--ink);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
}
.cast-character {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
}
/* on-disk rows share the deck's rel grammar: name leads, chips trail */
.disk-name {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--ink);
}
.rel[data-tone="fault"] {
color: var(--signal-fault);
}
/* ---- small screens --------------------------------------------------- */
@media (max-width: 46rem) {
@@ -1357,6 +1625,35 @@ body {
margin-left: auto;
}
/* the hero stacks: poster above the info block, both full width */
.movie-body {
flex-direction: column;
align-items: stretch;
gap: var(--space-3);
}
.movie-poster {
width: 8.5rem;
}
.movie-info {
padding-bottom: var(--space-4);
}
/* state controls wrap to their own line, errands stay right-aligned */
.movie-controls-space {
display: none;
}
.movie-root {
min-width: 0;
flex: 1;
}
.cast-grid {
grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
}
.board {
padding: var(--space-6) var(--space-4);
}