diff --git a/docs/design.md b/docs/design.md index ad2e870..9a9efb5 100644 --- a/docs/design.md +++ b/docs/design.md @@ -86,7 +86,7 @@ scale relative to surrounding text without changing the prose size. | Item or subsection title: project, paper, institution, article subsection | `--type-subheading` | 18px, Inter 500 | 1.4 | | Prose: body, summaries, record values, course names, table cells, captions | `--type-reading` | 16px, Source Serif 4 400 | 1.65 | | Interface: navigation, contents, Read More, degree names, diagram node titles | `--type-interface`, `--type-action` | 15px, Inter 400 (500 for actions and node titles) | 1.5 | -| Label: field labels, entry record lines, diagram stage and detail text, footer | `--type-label` | 13px, Inter 400 | 1.45 | +| Label: field labels, diagram stage and detail text, footer | `--type-label` | 13px, Inter 400 | 1.45 | | Section margin index, directory number, contribution number | `--type-index` | 11px, Source Serif 4 400 | 1.5 | | Inline mathematical expression | Relative to surrounding text | 1em | Intrinsic mathematical geometry | | Displayed mathematical expression | `--type-math` | 1.15em | Intrinsic mathematical geometry | @@ -99,10 +99,12 @@ manuscript render at weight 600, so each contribution opens with its claim; ital run-ins and bold theorem labels remain inside mathematics papers. The hero name is the one display exception. -Whitespace separates chapters and list items. Rules mark page-level sections, the -entries within a section, the two directories, the record strip, and diagram stages. -Do not add metric rows, pills, badges, or other pulled-out figures: a result belongs in -its sentence. +Subtraction is the rule. Each component uses at most two text styles, and small grey +labels appear only where a field needs naming (the record strip, contact details, +diagrams). Whitespace separates entries, directory rows, chapters and list items; +rules mark only page-level sections, the record strip, and diagram stages. Do not add +metric rows, pills, badges, or other pulled-out figures: a result belongs in its +sentence. Text roles use rem units and scale with the user's text setting. Titles and headings step down below 768px, including in the architecture viewer; the other roles remain @@ -119,7 +121,7 @@ Experience shares About's Inter headings, Source Serif 4 prose, role sizes, grid paper/ink palette. It is one page with five numbered sections and an anchor directory headed Experience: Areas of Focus that stays in one column at every screen width. Experience and Coursework use the same `SectionDirectory` component: full-width -rows separated by thin rules, with small serif numbers at the left and labels in the +rows separated by whitespace, with small serif numbers at the left and labels in the heading size. A right-aligned View action appears on hover and keyboard focus and remains visible on touch devices; the complete row turns blue on hover or focus. Directory numbers run from 01 through 05 for Experience @@ -129,19 +131,20 @@ link's accessible name. Section margin indices use the shared serif index role o About, Experience, and Coursework. Category introductions are omitted. On About, Experience, Coursework and the ten detail pages, content hangs on two axes -of the ten-column field (`--hang-columns`, 3fr / 7fr). From 1200px, item titles sit on -axis A and their prose on axis B, three columns in, separated by entry rules; below -1200px the title stacks above its prose. Each Experience entry shows its title in the -subheading role and, under it, a record line in the label role: the organization for -AI projects, the paper title for legal research, the position otherwise, then the -dates. These lines reuse the approved metadata verbatim. Summaries use upright black -body text. No content is truncated to fit a card. Sections end on the reading-end -guide (11 of the 12 content columns). +of the ten-column field (`--hang-columns`, 4fr / 6fr). From 1200px, item titles sit +alone on axis A and their prose on axis B, four columns in, with `--prose-inset` of +space before the reading-end guide; 64px of whitespace separates entries. Below 1200px +the title stacks above its prose. Summaries use upright black body text. No content is +truncated to fit a card. Sections end on the reading-end guide (11 of the 12 content +columns). All body copy uses the shared ink colors. Blue marks navigation, hover, keyboard focus, and the decision or feedback path in architecture figures. -AI entries lead with the project name. The confidentiality note closes the AI section. +AI entries lead with the project name. The confidentiality note introduces "the +following four projects", so it opens the AI section as its standfirst: directly under +the heading, on the same edge, in the reading face and supporting-ink colour. Its +heading is visually hidden and labels the aside for assistive technology. Each of the eight AI and legal articles opens with a `LogicDiagram` in its header, under the record strip, as the article's visual abstract. Its data lives in @@ -253,7 +256,7 @@ at the shared reading size and leading, in two columns on axis B from 1200px. Al wording and order as static text, available without JavaScript. The rail's Back to Education link returns to the Education section. Duke University appears once above the two degree records; -degree names use the interface size at weight 500, on axis B beside the institution. Compact notes and a thin rule +degree names use the interface size at weight 500, on axis B beside the institution. Compact notes and whitespace separate the degree records. Profile details and contact values also use the reading scale; field labels use the label scale. Email, GitHub, and X occupy three columns above 540px and stack in one column at 540px and below. Long contact values can wrap @@ -274,6 +277,8 @@ the shared title, heading, subheading and reading hierarchy; scoped spacing over Page titles own the H1 position. About's two-line "Theodore / Ouyang" supplies its main heading. The "Thinking mathematically / Working in AI" introduction is an H2 below the hero; it remains the H1 on the `/education/` compatibility page. +Its two lines share the title size, the second in the muted tone, and the biography +sits below on the same left edge within the reading measure. Experience domains use `01`–`05` indices; individual entries have no leading markers. Top-level contributions are plain paragraphs without bullets or rules. Narrative lines stay within the shared reading measure and reflow with the viewport. @@ -370,8 +375,8 @@ hover and keyboard-focus feedback. Titles and complete summaries remain outside the link. Summaries retain selected bold emphasis. Numerical guarantees retain their model, horizon, payoff, and comparison scope. It does not repeat the abstracts. Titles use the same -subheading role on axis A; supporting paragraphs use the reading role on axis B. Thin -horizontal rules separate entries. Typography, spacing, and wrapping adapt within +subheading role on axis A; supporting paragraphs use the reading role on axis B. +Whitespace separates entries. Typography, spacing, and wrapping adapt within the shared reading frame without introducing cards or changing the palette. Each research page provides a contents list and the manuscript's abstract, core diff --git a/website/app/components/SectionDirectory.module.css b/website/app/components/SectionDirectory.module.css index b9fa815..0eb4fba 100644 --- a/website/app/components/SectionDirectory.module.css +++ b/website/app/components/SectionDirectory.module.css @@ -11,9 +11,8 @@ align-items: baseline; gap: 16px; width: 100%; - min-height: 68px; - padding-block: 20px; - border-top: 1px solid var(--rule); + min-height: 52px; + padding-block: 10px; color: var(--ink); font-size: var(--type-heading); font-weight: 400; diff --git a/website/app/components/experience/Experience.module.css b/website/app/components/experience/Experience.module.css index 41ac059..79a4e35 100644 --- a/website/app/components/experience/Experience.module.css +++ b/website/app/components/experience/Experience.module.css @@ -33,7 +33,7 @@ .sectionIndex { grid-column: 1; padding-top: 0.5rem; } .sectionBody { grid-column: 2 / span 10; min-width: 0; } .sectionTitle { - margin: 0 0 40px; + margin: 0 0 56px; font-size: var(--type-heading); font-weight: 400; line-height: var(--heading-leading); @@ -41,13 +41,10 @@ text-wrap: balance; } -/* Entries hang their title and record on axis A and their summary on axis B. */ -.entry { - padding: 28px 0 32px; - border-top: 1px solid var(--rule-medium); - scroll-margin-top: 40px; -} -.entryHead { min-width: 0; } +/* Entries hang their title on axis A and their summary on axis B; whitespace, + not rules, separates them. */ +.entry { scroll-margin-top: 40px; } +.entry + .entry { margin-top: 64px; } .entryTitle { margin: 0; color: var(--ink); @@ -57,16 +54,6 @@ letter-spacing: var(--subheading-tracking); text-wrap: pretty; } -.entryMeta { - display: grid; - gap: 2px; - margin: 10px 0 0; - color: var(--muted); - font-size: var(--type-label); - line-height: var(--label-leading); - text-wrap: pretty; -} -.entryMeta span { display: block; } .entryBody { min-width: 0; max-width: var(--reading-measure); } .summary { margin: 0; font-size: var(--type-reading); line-height: var(--reading-leading); text-wrap: pretty; } .summary, @@ -77,18 +64,8 @@ font-family: var(--font-prose); } -.indexDisclosure { - padding: 28px 0 0; - border-top: 1px solid var(--rule-medium); -} -.indexDisclosure h3 { - margin: 0 0 8px; - color: var(--muted); - font-size: var(--type-label); - font-weight: 400; - line-height: var(--label-leading); - letter-spacing: 0; -} +/* The AI section's note is its standfirst: under the heading, on the same edge. */ +.indexDisclosure { max-width: var(--reading-measure); margin: -24px 0 72px; } .indexDisclosure p { color: var(--supporting-ink); } .disclosure h2 { margin: 0 0 8px; @@ -228,17 +205,16 @@ @media (min-width: 1200px) { .entry, - .indexDisclosure, .detailPage > .contributions { display: grid; grid-template-columns: var(--hang-columns); align-items: start; } - .entryHead, - .indexDisclosure h3, + .entryTitle, .detailPage > .contributions h2 { padding-right: var(--hang-gap); } - .entryBody { max-width: none; } + .entryBody { max-width: none; padding-right: var(--prose-inset); } .detailPage > .contributions h2 { margin: 0; } + .detailPage > .contributions > ol { padding-right: var(--prose-inset); } } @media (768px <= width < 1024px) { @@ -247,10 +223,9 @@ } @media (width < 1200px) { - .entryMeta { margin-bottom: 14px; } + .entryTitle { margin-bottom: 12px; } .metadata { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .detailPage > .contributions > ol, - .indexDisclosure p { max-width: var(--reading-measure); } + .detailPage > .contributions > ol { max-width: var(--reading-measure); } } @media (width < 768px) { @@ -260,8 +235,10 @@ .detailPage .detailHeader h1 { max-width: none; } .section { display: block; padding: 40px 0 56px; scroll-margin-top: 6rem; } .sectionIndex { display: block; margin-bottom: 10px; padding-top: 0; } - .sectionTitle { margin-bottom: 28px; } - .entry { padding: 24px 0 28px; scroll-margin-top: 6rem; } + .sectionTitle { margin-bottom: 32px; } + .indexDisclosure { margin: -8px 0 48px; } + .entry { scroll-margin-top: 6rem; } + .entry + .entry { margin-top: 48px; } .metadata { gap: 16px 24px; margin-top: 32px; } .contributions > ol > li { grid-template-columns: 1.75rem minmax(0, 1fr); } } diff --git a/website/app/components/experience/ExperienceContent.tsx b/website/app/components/experience/ExperienceContent.tsx index aa95e39..5793682 100644 --- a/website/app/components/experience/ExperienceContent.tsx +++ b/website/app/components/experience/ExperienceContent.tsx @@ -7,22 +7,10 @@ import styles from "./Experience.module.css"; function ReadingArrow() { return ; } -/** The record line under an entry title: who it was with (or the paper it produced), then when. */ -function entryRecord(entry: ShowcaseEntry) { - const context = entry.categoryId === "01" ? entry.organization - : entry.categoryId === "03" ? entry.detailTitle - : entry.metadata.position; - return [context, entry.metadata.dates]; -} function ExperienceEntry({ entry }: { entry: ShowcaseEntry }) { return (
-
-

{entry.listingTitle}

-

- {entryRecord(entry).map((line) => {line})} -

-
+

{entry.listingTitle}

{entry.categoryId === "01" ? : entry.summary}

@@ -39,11 +27,12 @@ export function ExperienceSection({ domain, standalone = false }: { domain: Show
{domain.name} - {domain.entries.map((entry) => )} + {/* The note introduces "the following four projects", so it opens the section. */} {domain.disclosure && } + {domain.entries.map((entry) => )}
); diff --git a/website/app/components/experience/LogicDiagram.module.css b/website/app/components/experience/LogicDiagram.module.css index de75dc4..6b62c67 100644 --- a/website/app/components/experience/LogicDiagram.module.css +++ b/website/app/components/experience/LogicDiagram.module.css @@ -5,8 +5,6 @@ container: diagram / inline-size; margin: 72px 0 0; - padding-top: 18px; - border-top: 1px solid var(--rule-strong); } .label { @@ -217,6 +215,11 @@ border-right: 1px solid var(--accent); } +/* The return rule under the stages between the loop's two ends. */ +.loopSpan { + min-width: 0; +} + .loopLabel { margin: 0; padding: 10px 12px 0; diff --git a/website/app/globals.css b/website/app/globals.css index fe85227..6849166 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -33,10 +33,13 @@ --title-tracking: -0.022em; --heading-tracking: -0.015em; --subheading-tracking: -0.006em; - /* Hanging layout: titles on axis A (3 of 10 columns), prose on axis B (7 of 10). - The gap belongs to the title cell so axis B lands exactly on a grid line. */ - --hang-columns: minmax(0, 3fr) minmax(0, 7fr); + /* Hanging layout: titles on axis A (4 of 10 columns), prose on axis B (6 of 10). + The gap belongs to the title cell so axis B lands exactly on a grid line, + and prose keeps an inset from the reading-end guide. */ + --hang-columns: minmax(0, 4fr) minmax(0, 6fr); + --hang-offset: 40%; --hang-gap: 40px; + --prose-inset: 32px; --link-underline-offset: 0.18em; --ease-out: cubic-bezier(0.23, 1, 0.32, 1); --drawer-ease: cubic-bezier(0.32, 0.72, 0, 1); @@ -111,6 +114,18 @@ button:focus-visible { background: var(--accent); } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + .skip-link { position: fixed; z-index: 200; @@ -189,7 +204,7 @@ button:focus-visible { } .structural-grid[data-guides="index"] .structural-guide-reading-measure { - grid-column: 8; + grid-column: 9; } @media (width < 1200px) { diff --git a/website/app/home.module.css b/website/app/home.module.css index 99e6b57..85332a7 100644 --- a/website/app/home.module.css +++ b/website/app/home.module.css @@ -64,9 +64,14 @@ display: block; } +/* A two-line statement: the second line steps back in tone, not in size. */ +.identity :is(h1, h2) span + span { + color: var(--muted); +} + .biographyCopy { max-width: var(--reading-measure); - margin-top: 32px; + margin-top: 40px; font-family: var(--font-prose); font-size: var(--type-reading); line-height: var(--reading-leading); @@ -82,21 +87,17 @@ } .sectionTitle { - margin: 0 0 40px; + margin: 0 0 56px; font-size: var(--type-heading); font-weight: 400; line-height: var(--heading-leading); letter-spacing: var(--heading-tracking); } -/* A hanging row: the item title on axis A, its prose on axis B. */ -.row { - padding: 28px 0 32px; - border-top: 1px solid var(--rule-medium); -} - -.row:last-child { - padding-bottom: 0; +/* A hanging row: the item title on axis A, its prose on axis B. Whitespace, + not rules, separates rows. */ +.row + .row { + margin-top: 64px; } .rowTitle, @@ -130,9 +131,7 @@ } .educationList :global(.education-entry) + :global(.education-entry) { - margin-top: 24px; - padding-top: 24px; - border-top: 1px solid var(--rule); + margin-top: 28px; } .educationList :global(.education-degree) { @@ -208,7 +207,6 @@ .contactStrip li + li { padding-left: 24px; - border-left: 1px solid var(--rule); } .contactLabel { @@ -259,11 +257,6 @@ } @media (min-width: 1200px) { - .biographyCopy { - max-width: none; - margin-left: 30%; - } - .row { display: grid; grid-template-columns: var(--hang-columns); @@ -277,15 +270,15 @@ .rowBody { max-width: none; + padding-right: var(--prose-inset); } .contactStrip { - grid-template-columns: minmax(0, 3fr) minmax(0, 3fr) minmax(0, 4fr); + grid-template-columns: minmax(0, 4fr) minmax(0, 3fr) minmax(0, 3fr); } .contactStrip li + li { padding-left: 0; - border-left: 0; } } @@ -341,8 +334,8 @@ margin-bottom: 28px; } - .row { - padding: 24px 0 28px; + .row + .row { + margin-top: 48px; } } @@ -353,9 +346,7 @@ } .contactStrip li + li { - padding: 20px 0 0; - border-top: 1px solid var(--rule); - border-left: 0; + padding: 0; } } diff --git a/website/content/protected-sources.json b/website/content/protected-sources.json index fc06828..04be0e9 100644 --- a/website/content/protected-sources.json +++ b/website/content/protected-sources.json @@ -4,7 +4,7 @@ "sources": { "app/experience/page.tsx": "1dbf2ee75f4d859128a5ce92b6a0c7c6df94579962c37bc672544c4d7698c3a6", "app/experience/[slug]/page.tsx": "b57c5422793ec804d64e3da7217762400bf682a8c71a9a7f850201b66c04d6c8", - "app/components/experience/ExperienceContent.tsx": "70c80dbc812293c4e38a902fea79fee7fb730f577c7fa2669529a881a1643960", + "app/components/experience/ExperienceContent.tsx": "fe93231a7f95db5f76b7d36260995a7745178734acc2ec139b9b9a16ff029c22", "app/components/experience/ExperienceDetails.tsx": "4d6320c1adf2310a75dea9cb6b4fca037f166b0073da5b5627e4e4ef8a3a3385", "app/lib/content/experience-showcase.ts": "2711d4c142e9a48c2cc2e7b0480a6827bba0c4d7898f3b9e3d42918b9b08a544", "app/lib/content/experience-domains.ts": "7362085d4857daa83ae476f176163378dde550572d5a1932d2f142d4d1a00c7d", diff --git a/website/content/visible-copy-manifest.json b/website/content/visible-copy-manifest.json index b7ae676..e1f0d53 100644 --- a/website/content/visible-copy-manifest.json +++ b/website/content/visible-copy-manifest.json @@ -775,9 +775,9 @@ "formulaCount": 973 }, "/experience/": { - "textSha256": "78cb36b17f5a155a4e9916e427c238ac811b0b3f2d78d43d43f97c30c68dd9c8", - "characterCount": 10317, - "wordCount": 1369, + "textSha256": "4674ae73ddb7b22bb5a00935889e34e0e14630541241bdf5239c059c37cb13c0", + "characterCount": 8870, + "wordCount": 1156, "preview": "Skip to main content Theodore Ouyang About Experience Experience: Areas of Focus AI Research and Engineering Data Science Legal Research and Policy Analysis Fin", "altTexts": [], "ariaLabels": [ @@ -1025,9 +1025,9 @@ } }, "/past-experience/": { - "textSha256": "78cb36b17f5a155a4e9916e427c238ac811b0b3f2d78d43d43f97c30c68dd9c8", - "characterCount": 10317, - "wordCount": 1369, + "textSha256": "4674ae73ddb7b22bb5a00935889e34e0e14630541241bdf5239c059c37cb13c0", + "characterCount": 8870, + "wordCount": 1156, "preview": "Skip to main content Theodore Ouyang About Experience Experience: Areas of Focus AI Research and Engineering Data Science Legal Research and Policy Analysis Fin", "altTexts": [], "ariaLabels": [ @@ -1065,10 +1065,10 @@ } }, "/past-experience/artificial-intelligence/": { - "textSha256": "eef9a3bc27efdc436856c74094a53e593440032adc1cf97396190ba7779599d4", - "characterCount": 3292, - "wordCount": 442, - "preview": "Skip to main content Theodore Ouyang Back to Experience AI Research and Engineering Large-scale domain expert feedback governance and adaptive review Duke Unive", + "textSha256": "0eee330e465ec7fa8e9a490d759ed9b0df7d96de12a833889c486280b284f37c", + "characterCount": 2945, + "wordCount": 393, + "preview": "Skip to main content Theodore Ouyang Back to Experience AI Research and Engineering Confidentiality & Disclosure Note I conducted the following four projects th", "altTexts": [], "ariaLabels": [ "Theodore Ouyang home", @@ -1327,10 +1327,10 @@ "formulaCount": 136 }, "/past-experience/data-science/": { - "textSha256": "6a0cd7abc26180586f454a18267837381bc2e650b15a761e83cd29129a006064", - "characterCount": 1351, - "wordCount": 182, - "preview": "Skip to main content Theodore Ouyang Back to Experience Data Science Jiritsu Network Business Development and Business Analytics (Full-time) August 2024 – Octob", + "textSha256": "4c28243e2463e440a5821cbeb48f0044ad1a042b262bc5e841ebea2f585cf666", + "characterCount": 1187, + "wordCount": 157, + "preview": "Skip to main content Theodore Ouyang Back to Experience Data Science Jiritsu Network Owned business development for Jiritsu's asset-verification and privacy-pre", "altTexts": [], "ariaLabels": [ "Theodore Ouyang home", @@ -1351,10 +1351,10 @@ } }, "/past-experience/legal-research-and-policy-analysis/": { - "textSha256": "e37047334b031b87edad40697698671aaf74cce0c54d102afa0e087e57251ad6", - "characterCount": 3244, - "wordCount": 432, - "preview": "Skip to main content Theodore Ouyang Back to Experience Legal Research and Policy Analysis Duke Law School: Space Law and Autonomous Systems Research Autonomous", + "textSha256": "e4c6c4d6f51fe85467925f1bf156ee7eca9e5f4223daa6fbe7189e4de7561d63", + "characterCount": 2769, + "wordCount": 364, + "preview": "Skip to main content Theodore Ouyang Back to Experience Legal Research and Policy Analysis Duke Law School: Space Law and Autonomous Systems Research I develope", "altTexts": [], "ariaLabels": [ "Theodore Ouyang home", @@ -1376,10 +1376,10 @@ } }, "/past-experience/finance/": { - "textSha256": "71e835a59414372df382e4a9b7fd98ca5b30e914b9aa42b263e58a5f444f16df", - "characterCount": 1809, - "wordCount": 241, - "preview": "Skip to main content Theodore Ouyang Back to Experience Finance and Consulting Jones Lang LaSalle Capital Markets Team Summer Intern June 2024 – August 2024 Ser", + "textSha256": "2fb0fa2cc1489d8c7a9c2960e6b5e831cdd33e4acbe1fe7cfac26360e6390e8f", + "characterCount": 1552, + "wordCount": 199, + "preview": "Skip to main content Theodore Ouyang Back to Experience Finance and Consulting Jones Lang LaSalle Capital Markets Team Served as the project team's ESG lead on ", "altTexts": [], "ariaLabels": [ "Theodore Ouyang home", @@ -1402,10 +1402,10 @@ } }, "/past-experience/stem-academic-competitions-and-training/": { - "textSha256": "56eefd34f7bb0b901911ae40ed5c347c7445b64e3b62937d808831a034193a00", - "characterCount": 837, - "wordCount": 114, - "preview": "Skip to main content Theodore Ouyang Back to Experience STEM Academic Competitions and Training Independent STEM Olympiad Training Practice IPhO-Level Physics I", + "textSha256": "6d84e5a156f15f08ffedf0488b840b1240b579e3c30675a9fb48405441c8c3b3", + "characterCount": 633, + "wordCount": 85, + "preview": "Skip to main content Theodore Ouyang Back to Experience STEM Academic Competitions and Training Independent STEM Olympiad Training Practice Independently coache", "altTexts": [], "ariaLabels": [ "Theodore Ouyang home", @@ -1787,9 +1787,9 @@ } }, "/now/": { - "textSha256": "78cb36b17f5a155a4e9916e427c238ac811b0b3f2d78d43d43f97c30c68dd9c8", - "characterCount": 10317, - "wordCount": 1369, + "textSha256": "4674ae73ddb7b22bb5a00935889e34e0e14630541241bdf5239c059c37cb13c0", + "characterCount": 8870, + "wordCount": 1156, "preview": "Skip to main content Theodore Ouyang About Experience Experience: Areas of Focus AI Research and Engineering Data Science Legal Research and Policy Analysis Fin", "altTexts": [], "ariaLabels": [ diff --git a/website/tests/rendered-html.test.mjs b/website/tests/rendered-html.test.mjs index b0c84cd..5278462 100644 --- a/website/tests/rendered-html.test.mjs +++ b/website/tests/rendered-html.test.mjs @@ -679,13 +679,7 @@ test("all eighteen approved Experience entries use institution titles and real l assert.equal(summaries.length, 1); if (entry.categoryId === "01") assertApprovedInline(summaries[0][2], entry.summary); else assert.equal(stripMarkup(summaries[0][2]), normalizeSpace(entry.summary)); - assert.equal(openingTags(rendered, "p").length, 2, `Entry ${entry.id} must have one outer summary and one record line`); - const record = [...rendered.matchAll(/]*)>([\s\S]*?)<\/p>/giu)] - .find((match) => attribute(match[1], "data-experience-record") !== undefined); - assert.ok(record, `Entry ${entry.id} needs its record line under the title`); - const recordContext = entry.categoryId === "01" ? entry.organization : entry.categoryId === "03" ? entry.detailTitle : entry.metadata.position; - assert.deepEqual([...record[2].matchAll(/]*>([\s\S]*?)<\/span>/giu)].map((match) => stripMarkup(match[1])), - [normalizeSpace(recordContext), normalizeSpace(entry.metadata.dates)], "The record line reuses the approved metadata verbatim"); + assert.equal(openingTags(rendered, "p").length, 1, `Entry ${entry.id} must have one outer summary`); assert.equal(stripMarkup(rendered.match(/]*>([\s\S]*?)<\/h3>/u)?.[1] ?? ""), presentation.listingTitle); const links = [...rendered.matchAll(/]*)>([\s\S]*?)<\/a>/giu)]; assert.equal(links.length, 1, `Entry ${entry.id} needs one Read More action`); @@ -1035,10 +1029,14 @@ test("the production design contract is restrained and dependency-light", async assert.match(directory, /\.directory\s*\{[^}]*grid-template-columns:\s*1fr/u); assert.match(experience, /\.section\s*\{[^}]*grid-template-columns:\s*repeat\(11,[^}]*grid-column:\s*1 \/ span 11/u, "Sections end on the reading-end guide"); assert.match(experience, /\.sectionBody\s*\{[^}]*grid-column:\s*2\s*\/\s*span\s*10/u); - assert.match(globals, /--hang-columns:\s*minmax\(0,\s*3fr\)\s+minmax\(0,\s*7fr\)/u, "Hanging rows put titles on 3 of 10 columns and prose on 7"); + assert.match(globals, /--hang-columns:\s*minmax\(0,\s*4fr\)\s+minmax\(0,\s*6fr\)/u, "Hanging rows put titles on 4 of 10 columns and prose on 6"); for (const css of [experience, home]) assert.match(css, /@media \(min-width: 1200px\)[\s\S]*?grid-template-columns:\s*var\(--hang-columns\)/u); assert.match(structuralGrid, /data-guides=\{layout\}/u); - assert.match(globals, /\.structural-grid\[data-guides="index"\] \.structural-guide-reading-measure\s*\{\s*grid-column:\s*8/u, "Index pages mark the prose axis"); + assert.match(globals, /\.structural-grid\[data-guides="index"\] \.structural-guide-reading-measure\s*\{\s*grid-column:\s*9/u, "Index pages mark the prose axis"); + // Whitespace, not rules, separates entries and directory rows; rules stay at section level. + assert.doesNotMatch(experience, /(?:^|\n)\.entry(?: \+ \.entry)?\s*\{[^}]*border/u); + assert.doesNotMatch(home, /\.row\s*\{[^}]*border/u); + assert.doesNotMatch(directory, /\.directory a\s*\{[^}]*border/u); assert.doesNotMatch(globals, /\.education-entry\s*\{[^}]*grid-template-columns/u); for (const guide of ["frame-start", "rail-boundary", "reading-start", "reading-measure", "reading-end"]) assert.ok(structuralGrid.includes(`"${guide}"`)); for (const contract of ["aria-controls=\"site-menu\"", "aria-expanded={menuOpen}", 'event.key === "Escape"', 'document.body.classList.add("menu-open")', 'querySelectorAll']) assert.ok(navigation.includes(contract));