diff --git a/build/Vita.hs b/build/Vita.hs index f7fb4eb..e0f8476 100644 --- a/build/Vita.hs +++ b/build/Vita.hs @@ -27,7 +27,7 @@ module Vita import Data.Aeson (FromJSON (..), Object, Value (..), withObject, (.:), (.:?), (.!=)) import Data.Aeson.Types (Parser, typeMismatch) import Data.Char (toLower) -import Data.List (isPrefixOf, sortOn) +import Data.List (intercalate, isPrefixOf, sortOn) import Data.Maybe (mapMaybe) import Data.Scientific (isInteger, toRealFloat) import qualified Data.Aeson.Key as K @@ -360,22 +360,48 @@ rewriteCmd2 name f = go dateRange :: String -> Maybe String -> String dateRange s me = tex s ++ maybe "" (\e -> " – " ++ tex e) me --- | The grey line under an entry heading: dates, then location if present. -metaLine :: String -> Maybe String -> Maybe String -> String -metaLine dates mloc murl = concat - [ "

" - , "", dates, "" - , case mloc of - Nothing -> "" - Just loc -> concat - [ " · " - , case murl of - Just u -> "" ++ tex loc ++ "" - Nothing -> "" ++ tex loc ++ "" - ] - , "

" +-- | Title and date on one baseline, title left and date flush right — the +-- layout item-card.css already establishes for every other list on the +-- site. An earlier version stacked the date on its own line below the +-- title, which read as a stray indented fragment: @#markdownBody p + p@ +-- applies the essay prose indent of 1.5em to consecutive paragraphs, and a +-- date is not prose. +-- +-- Dates stay in a @span@ rather than a @time@ element on purpose. Half of +-- them are "Fall 2024", "expected 2028", "Present" — no valid @datetime@ +-- value exists for those, and a @time@ without one is worse than no @time@. +headerRow :: String -> String -> String +headerRow titleHtml dates = concat + [ "
" + , "

", titleHtml, "

" + , "", dates, "" + , "
" ] +-- | The line under the header: role or degree, then whatever secondary facts +-- the entry carries (location, GPA), middot-separated in a quieter ink so +-- the role still leads. +subLine :: Maybe String -> [String] -> String +subLine mrole extras + | null parts = "" + | otherwise = "

" ++ lead ++ trailing ++ "

" + where + parts = maybe [] (pure . tex) mrole ++ extras + lead = head parts + rest = tail parts + trailing + | null rest = "" + | otherwise = " · " + ++ intercalate " · " rest + ++ "" + +-- | A location, linked when the entry gives a URL for it. +locationPart :: Maybe String -> Maybe String -> [String] +locationPart Nothing _ = [] +locationPart (Just loc) murl = pure $ case murl of + Just u -> "" ++ tex loc ++ "" + Nothing -> tex loc + -- | Link chips. The affordance the print CV cannot offer: every artifact one -- click away, rather than a bracketed label the reader has to retype. renderLinks :: [Link] -> String @@ -425,11 +451,11 @@ renderEducation es one e = concat [ "
  • " , "
    " - , "

    ", tex (edInstitution e), "

    " - , "

    ", tex (edDegree e) - , maybe "" (\g -> " · GPA " ++ tex g ++ "") (edGpa e) - , "

    " - , metaLine (dateRange (edStart e) (edEnd e)) (edLocation e) Nothing + , headerRow (tex (edInstitution e)) (dateRange (edStart e) (edEnd e)) + , subLine (Just (edDegree e)) + ( maybe [] (\g -> ["GPA " ++ tex g]) (edGpa e) + ++ locationPart (edLocation e) Nothing + ) , maybe "" (\n -> "

    " ++ tex n ++ "

    ") (edNotes e) , "
    " , "
  • " @@ -459,17 +485,16 @@ renderPublications ps -- Entries without a title (work in preparation) put the label in -- the authors field; the CV template makes the same distinction. Nothing -> concat - [ "

    ", tex (pbAuthors p), "

    " + [ headerRow (tex (pbAuthors p)) (dateOf p) , "

    ", tex (pbVenue p) , maybe "" (\t -> " " ++ tex t) (pbTarget p) , "

    " ] Just t -> concat - [ "

    ", tex t, "

    " + [ headerRow (tex t) (dateOf p) , "

    ", tex (pbAuthors p), "

    " , "

    ", tex (pbVenue p), "

    " ] - , "

    ", dateOf p, "

    " , renderLinks (pbLinks p) , "" , "" @@ -489,14 +514,12 @@ renderPresentations ps one p = concat [ "
  • " , "
    " - , "

    ", tex (prTitle p), "

    " + , headerRow (tex (prTitle p)) (dateOf p) , "

    ", tex (prAuthors p), "

    " , "

    " , maybe "" (\k -> tex k ++ ", ") (prKind p) , tex (prVenue p) - , "

    " - , "

    ", dateOf p, "" - , maybe "" (\s -> "" ++ tex s ++ "") (prStatus p) + , maybe "" (\st -> "" ++ tex st ++ "") (prStatus p) , "

    " , "
    " , "
  • " @@ -522,9 +545,8 @@ renderExperience xs = research ++ industry one e = concat [ "
  • " , "
    " - , "

    ", tex (exOrg e), "

    " - , maybe "" (\r -> "

    " ++ tex r ++ "

    ") (exRole e) - , metaLine (dateRange (exStart e) (exEnd e)) (exLocation e) (exLocUrl e) + , headerRow (tex (exOrg e)) (dateRange (exStart e) (exEnd e)) + , subLine (exRole e) (locationPart (exLocation e) (exLocUrl e)) , maybe "" (\p -> "

    " ++ tex p ++ "

    ") (exPreamble e) , renderBullets (exBullets e) , "
    " @@ -563,12 +585,11 @@ renderProjects ps = concatMap one (groupOrder visible) entry p = concat [ "
  • " , "
    " - , "

    " - , case pjEssay p of - Just u -> "" ++ tex (pjName p) ++ "" - Nothing -> tex (pjName p) - , "

    " - , metaLine (dateRange (pjStart p) (pjEnd p)) Nothing Nothing + , headerRow + (case pjEssay p of + Just u -> "" ++ tex (pjName p) ++ "" + Nothing -> tex (pjName p)) + (dateRange (pjStart p) (pjEnd p)) , "

    ", tex (pjDescription p), "

    " , renderLinks (pjLinks p) , "
    " diff --git a/static/css/vita.css b/static/css/vita.css index abdbc21..8131f1d 100644 --- a/static/css/vita.css +++ b/static/css/vita.css @@ -1,33 +1,41 @@ -/* vita.css — /about.html. +/* vita.css — /about.html and /cv/projects/. * * The vita is a record surface. Where Now answers "what is moving right - * now" with a status chip per item, the vita answers "what is on file" - * and carries no temporality beyond the dates themselves — so it borrows - * item-card.css's card rhythm and now.css's section headings, but drops - * the badge column entirely. Entries here have no state to report. + * now" with a status chip per item, the vita answers "what is on file" and + * carries no temporality beyond the dates themselves — so it borrows + * item-card.css's card rhythm and now.css's section headings, but drops the + * badge column entirely. Entries here have no state to report. * - * The one affordance this page has that the CV PDF cannot: every artifact - * is one click away. Link chips are therefore the only element given any - * interaction polish; everything else stays deliberately quiet so the - * chips read as the page's active surface. + * SPECIFICITY NOTE. Almost every selector below is prefixed with + * #markdownBody, and that is load-bearing rather than habit. typography.css + * styles the prose surface through #markdownBody (1,0,1) — including + * `:is(h1..h6)` at display sizes with 2.475rem margins, and a 1.5em + * text-indent on `p + p` for essay prose. A bare `.vita-entry-title` (0,1,0) + * loses to those no matter what order the stylesheets load in, which is + * exactly what went wrong the first time: entry titles rendered at 1.45rem + * sans with heading margins, and every second paragraph in a card sat + * indented like a continuation of running prose. + * + * Now sidesteps this by never using heading elements inside its cards. The + * vita keeps them — a record page wants a real document outline for anyone + * navigating by heading — and pays for them with specificity instead. */ /* ============================================================ INTRO PROSE - Body content from about.md — the pointers and the four research - threads — set above the generated sections. Mirrors .now-intro - so the two data-driven pages open in the same register. + Body content from the markdown — pointers and the research + threads on /about, the framing note on the project index. + Mirrors .now-intro so the data-driven pages open alike. ============================================================ */ -.vita-intro { - margin: 0 0 3rem; +#markdownBody .vita-intro { + margin: 0 0 2.75rem; font-family: var(--font-serif); - font-size: 1rem; color: var(--text-muted); line-height: 1.6; } -.vita-intro h2 { +#markdownBody .vita-intro h2 { font-family: var(--font-serif); font-size: 1.15rem; font-variant: all-small-caps; @@ -35,25 +43,32 @@ letter-spacing: 0.09em; color: var(--text-muted); font-weight: 400; - margin: 2rem 0 0.6rem; + margin: 1.9rem 0 0.5rem; } -.vita-intro p:last-child { - margin-bottom: 0; +#markdownBody .vita-intro > p:first-of-type { + margin-top: 0; +} + +/* The intro is a framing note, not running prose, so it opts out of the + essay indent on consecutive paragraphs too. Two short paragraphs where + the second starts 1.5em in reads as a mistake rather than a convention. */ +#markdownBody .vita-intro p { + text-indent: 0; } /* ============================================================ SECTIONS - Identical treatment to .now-section — same size, tracking, and + Identical treatment to .now-section — same size, tracking and ink — because the two pages are siblings and a reader moving between them should not have to re-learn the hierarchy. ============================================================ */ -.vita-section { - margin-bottom: 2.75rem; +#markdownBody .vita-section { + margin: 0 0 2.5rem; } -.vita-section-heading { +#markdownBody .vita-section-heading { font-family: var(--font-serif); font-size: 1.15rem; font-variant: all-small-caps; @@ -62,89 +77,75 @@ color: var(--text-muted); text-transform: none; font-weight: 400; - margin: 0 0 0.85rem 0; + margin: 0 0 0.7rem 0; + line-height: 1.3; } /* ============================================================ ENTRIES .vita-card composes on .item-card, which is a flex row sized for a badge column. The vita emits no badge, so the single - .item-card-main child simply takes the full width. + .item-card-main child takes the full width. ============================================================ */ -.vita-list { +#markdownBody .vita-card { + padding: 0.8rem 0; +} + +/* Kill the prose indent inside cards outright. Card paragraphs are + labelled data — a role, a venue, an author list — not successive + paragraphs of an essay, and the 1.5em indent reads as damage. */ +#markdownBody .vita-card p { + text-indent: 0; margin: 0; } -.vita-entry-title { +/* The title. Serif at body size, matching .item-card-title rather than any + heading step: these are list entries that happen to be headings for + outline purposes, not section openers. */ +#markdownBody .vita-entry-title { font-family: var(--font-serif); font-size: 1rem; font-weight: 600; - color: var(--text); line-height: 1.35; + color: var(--text); margin: 0; + font-feature-settings: normal; } -/* Degree, role, position. Serif italic — the scholarly convention - for a title held, and it separates the role from the institution - above it without another weight or size step. */ -.vita-role { - font-family: var(--font-serif); - font-size: 0.95rem; - font-style: italic; - color: var(--text-muted); - margin: 0.15rem 0 0; - line-height: 1.4; +#markdownBody .vita-entry-title a { + color: inherit; + text-decoration: none; } -.vita-gpa { - font-style: normal; - color: var(--text-faint); +#markdownBody .vita-entry-title a:hover { + text-decoration: underline; + text-underline-offset: 0.15em; } -/* Author lists run long and are reference material rather than - reading material — sans, small, muted, tight. */ -.vita-authors { - font-family: var(--font-sans); - font-size: 0.82rem; - color: var(--text-muted); - margin: 0.2rem 0 0; - line-height: 1.45; -} - -/* Venue: journal, conference, or publication state. Italic serif is - the citation convention and distinguishes it from the author line - directly above without adding a color step. */ -.vita-venue { +/* Role, degree, position — and any secondary facts hung off it. Serif + italic is the scholarly convention for a title held, and it separates + the role from the institution above without another size step. */ +#markdownBody .vita-role { font-family: var(--font-serif); font-size: 0.92rem; font-style: italic; color: var(--text-muted); - margin: 0.2rem 0 0; + margin: 0.1rem 0 0; line-height: 1.45; } -/* Dates and location. Matches .item-card-date's register exactly — - tabular numerals so date columns stay optically aligned down the - list even though they are inline here. */ -.vita-meta { +/* Location and GPA trail the role in a fainter ink so the role still + leads the line. */ +#markdownBody .vita-quiet { + color: var(--text-faint); + font-style: normal; font-family: var(--font-sans); - font-size: 0.72rem; - color: var(--text-faint); - margin: 0.25rem 0 0; - font-variant-numeric: tabular-nums; - line-height: 1.5; + font-size: 0.78rem; } -.vita-sep { - user-select: none; -} - -.vita-location { - color: var(--text-faint); -} - -a.vita-location { +#markdownBody .vita-quiet a.vita-location { + color: inherit; text-decoration: underline; text-decoration-color: var(--border); text-decoration-thickness: 0.08em; @@ -152,30 +153,52 @@ a.vita-location { transition: color var(--transition-fast), text-decoration-color var(--transition-fast); } -a.vita-location:hover { +#markdownBody .vita-quiet a.vita-location:hover { color: var(--text-muted); text-decoration-color: var(--border-muted); } -/* Presentation state ("Presented", "Accepted"). Sits on the meta - line rather than in a badge column, so it is set as small-caps - text with a leading separator instead of a bordered chip — a - frame here would compete with the link chips below it. */ -.vita-status { +/* Author lists run long and are reference material rather than reading + material — sans, small, muted, tight. */ +#markdownBody .vita-authors { font-family: var(--font-sans); - font-variant: all-small-caps; - letter-spacing: 0.07em; + font-size: 0.78rem; color: var(--text-muted); + margin: 0.25rem 0 0; + line-height: 1.45; } -.vita-status::before { - content: " · "; +/* Venue: journal, conference, or publication state. Italic serif is the + citation convention and distinguishes it from the author line above + without adding a colour step. */ +#markdownBody .vita-venue { + font-family: var(--font-serif); + font-size: 0.9rem; + font-style: italic; + color: var(--text-muted); + margin: 0.1rem 0 0; + line-height: 1.45; +} + +/* Presentation state ("Presented", "Accepted"). Sits at the end of the + venue line as small-caps text rather than a bordered chip — a frame + here would compete with the link chips below it. */ +#markdownBody .vita-status { + font-family: var(--font-sans); + font-size: 0.72rem; + font-style: normal; + font-variant: all-small-caps; + letter-spacing: 0.07em; color: var(--text-faint); } -/* Preamble / note lines — the CV's connective prose. Same register - as .item-card-abstract.is-full, unclamped. */ -.vita-note { +#markdownBody .vita-status::before { + content: " · "; +} + +/* Preamble, notes, project descriptions — the connective prose. Same + register as .item-card-abstract.is-full, unclamped. */ +#markdownBody .vita-note { font-family: var(--font-sans); font-size: var(--text-size-small); color: var(--text-muted); @@ -183,50 +206,50 @@ a.vita-location:hover { line-height: 1.55; } -.vita-bullets { +#markdownBody .vita-bullets { margin: 0.4rem 0 0; - padding-left: 1.05rem; + padding-left: 0.85rem; list-style: none; } -.vita-bullets li { +#markdownBody .vita-bullets li { font-family: var(--font-sans); font-size: var(--text-size-small); color: var(--text-muted); line-height: 1.55; - margin: 0.3rem 0 0; + margin: 0.25rem 0 0; position: relative; } -/* Hairline dash rather than a bullet glyph: the entries are claims, - not an enumerated sequence, and a disc would assert more structure - than the content has. */ -.vita-bullets li::before { +/* Hairline dash rather than a bullet glyph: these are claims, not an + enumerated sequence, and a disc would assert more structure than the + content has. */ +#markdownBody .vita-bullets li::before { content: ""; position: absolute; - left: -1.05rem; - top: 0.72em; - width: 0.5rem; + left: -0.85rem; + top: 0.7em; + width: 0.4rem; height: 1px; background: var(--border-muted); } /* ============================================================ LINK CHIPS - The page's one interactive surface, and the reason a vita on the - web beats a vita on paper. Geometry is lifted from .now-status so - the chip shape is already familiar from /current; the difference - is that these are links and therefore respond. + The page's one interactive surface, and the reason a vita on + the web beats a vita on paper. Geometry is lifted from + .now-status so the shape is already familiar from /current; + the difference is that these are links, and respond. ============================================================ */ -.vita-links { +#markdownBody .vita-links { margin: 0.5rem 0 0; display: flex; flex-wrap: wrap; - gap: 0.35rem; + gap: 0.3rem; } -.vita-chip { +#markdownBody .vita-chip { display: inline-block; font-family: var(--font-sans); font-size: 0.66rem; @@ -242,63 +265,56 @@ a.vita-location:hover { text-decoration: none; transition: color var(--transition-fast), - border-color var(--transition-fast), - background-color var(--transition-fast); + border-color var(--transition-fast); } -.vita-chip:hover { +#markdownBody .vita-chip:hover { color: var(--text); border-color: var(--text-muted); - background: var(--surface-raised, transparent); } -.vita-chip:focus-visible { +#markdownBody .vita-chip:focus-visible { outline: 2px solid var(--text-muted); outline-offset: 2px; } -/* The equal-contribution legend. Belongs to the section, not to any - one entry, so it is set below the list in the faintest register - the page uses. */ -.vita-footnote { +/* The equal-contribution legend. Belongs to the section rather than to + any one entry, so it sits below the list in the faintest register. */ +#markdownBody .vita-footnote { font-family: var(--font-serif); font-size: 0.85rem; font-style: italic; color: var(--text-faint); - margin: 0.9rem 0 0; + margin: 0.8rem 0 0; + text-indent: 0; } /* ============================================================ MOBILE (≤540px) - item-card.css already stacks its header at this breakpoint. The - vita's entries are single-column by construction, so only the - type steps down. + item-card.css already stacks .item-card-header and drops the + date below the title at this width, so only the type steps. ============================================================ */ @media (max-width: 540px) { - .vita-entry-title { + #markdownBody .vita-entry-title { font-size: 0.95rem; overflow-wrap: anywhere; } - .vita-role, - .vita-venue { - font-size: 0.9rem; + #markdownBody .vita-role, + #markdownBody .vita-venue { + font-size: 0.88rem; } - .vita-meta { - font-size: 0.68rem; - } - - .vita-chip { + #markdownBody .vita-chip { font-size: 0.62rem; padding: 0.28em 0.45em 0.24em; } } @media (prefers-reduced-motion: reduce) { - .vita-chip, - a.vita-location { + #markdownBody .vita-chip, + #markdownBody .vita-quiet a.vita-location { transition: none; } }