PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m12s
PR Checks / Backend Smoke (pull_request) Successful in 9s
PR Checks / Build Backend (no push) (pull_request) Successful in 32s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m9s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m15s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m26s
Review findings on #140. Drafts were reachable. Posts granted unconditional public read and the _status filter lived only in the pages that query the collection — which is a convenience, not a control. Payload's documentation is explicit: "The `draft` argument alone does not restrict documents with _status: 'draft' from being returned by the API." A direct GET /cms-api/posts would have handed every unpublished draft to any visitor. Read access now returns a query constraint for anonymous callers, which is the documented mechanism. The --drop claim was asserted across four files while the spec still listed it as an open question. Now verified rather than assumed: run_full_migration drops exactly ["school_results", "schools"] by name, there is no drop_all() or DROP SCHEMA anywhere in backend/, the only other drop is schema-qualified to marts, and nothing sets search_path. The guarantee is stronger than schema isolation alone — those two table names do not exist in Payload — so the claim stands, but it now rests on cited code. The spec records the evidence and closes the open item. findPost is wrapped in React's cache(): Next calls generateMetadata and the page separately for one request, so every post view ran the same query against Postgres twice. The bare .lede rule was dead — .prose p scores (0,1,1) and outranks it — so only .prose .lede ever applied. Removed, with the specificity noted so the surviving selector is not "simplified" back into a silent regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017YmbBhr8s7GusjDE12hrZM
83 lines
1.5 KiB
CSS
83 lines
1.5 KiB
CSS
.page {
|
|
max-width: 42rem;
|
|
margin: 0 auto;
|
|
padding: 2.5rem 1.25rem 4rem;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1.25rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.portrait {
|
|
border-radius: 50%;
|
|
border: 2px solid var(--border);
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.kicker {
|
|
font-family: var(--font-ui);
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--brand);
|
|
margin: 0 0 0.35rem;
|
|
}
|
|
|
|
.heading {
|
|
font-family: var(--font-display);
|
|
font-size: clamp(1.5rem, 4vw, 2rem);
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
.subheading {
|
|
font-family: var(--font-display);
|
|
font-size: 1.15rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin: 2.25rem 0 0.75rem;
|
|
}
|
|
|
|
.prose p {
|
|
font-family: var(--font-ui);
|
|
font-size: 1rem;
|
|
line-height: 1.7;
|
|
color: var(--text-secondary);
|
|
margin: 0 0 1.1rem;
|
|
}
|
|
|
|
/* The opening paragraph carries the page. Larger, and in the primary ink
|
|
rather than the secondary, so it reads as a voice rather than as body copy.
|
|
|
|
Must stay in the descendant form: `.prose p` scores (0,1,1) and would beat a
|
|
bare `.lede` at (0,1,0), so simplifying this selector silently reverts the
|
|
lede to ordinary body copy. */
|
|
.prose .lede {
|
|
font-size: 1.125rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.link {
|
|
color: var(--brand);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.link:hover {
|
|
color: var(--brand-strong);
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.header {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 1rem;
|
|
}
|
|
}
|