Migrating UPS "About" site (https://about.ups.com/us/en/home.html) to Adobe Edge Delivery Services.
- NEVER create screenshots outside
/tmpfolder - All screenshots MUST be saved to/tmp/directory. Never save screenshots to project root or any workspace folder. - Always read files before editing - Never modify code without reading it first.
- Use
box-sizing: border-box- When setting explicit width/height on elements with padding. - REUSE existing blocks - Always use existing blocks and variants before creating new ones. See "Block Reuse Guidelines" section.
- Keep PROJECT.md up-to-date - Update this file when creating/modifying/deleting blocks, variants, or patterns. See "Maintaining This Documentation" section.
- Create variants, not new blocks - When a content pattern is similar to an existing block but needs different styling, create a VARIANT of that block (not a new block). This maintains consistency and reduces code duplication.
- Never import all-caps content as-is - When source content is ALL CAPS in the DOM (e.g., "REPORTS AND DISCLOSURES"), convert it to Title Case or Sentence case in the HTML content and apply
text-transform: uppercasevia CSS instead. This preserves authoring flexibility and avoids requiring authors to type in all caps. - Don't rely on bold/strong for block-wide styling - If an entire text element in a block needs to be bold or styled differently (like eyebrow labels or attribution text), apply
font-weight: 700via CSS targeting the element position (e.g.,:first-child). Reserve<strong>only for inline emphasis where the author wants to distinguish specific words from surrounding text. - Keep import scripts aligned with content
.plain.html- When changing content markup patterns, update all related parsers intools/importer/parsers/. Content.plain.htmlis the source of truth; parsers must reproduce it exactly. See "Import Script Alignment" in Migration Rules. - NEVER push HTML content via Git - Content and code are strictly separated. Content lives in the CMS (DA), code lives in Git. Never add
.htmlfiles to Git, never modify.gitignoreto track HTML files. See "Content Architecture" section. - NEVER commit or push to Git yourself - The user handles all Git operations (commit, push, branch management). Only make code changes to files — leave staging, committing, and pushing to the user.
- Code must be compatible with DA markup - DA (Document Authoring) wraps inline content in
<p>tags. Block JS and CSS must handle this gracefully with flexible selectors — never add JS workarounds to unwrap DA markup. See "DA Markup Compatibility" section. .plain.htmlis the single source of truth - All content edits and updates are made directly to.plain.htmlfiles in/content/. No.htmlfiles exist in the content folder..plain.htmluses div-format blocks (<div class="block-name">) and section<div>wrappers — this is the format DA consumes. See "Content Architecture" section.- Keep
/sitemap.jsonup-to-date at all times - Update the sitemap whenever pages are discovered, imported, re-imported, refactored, validated, critiqued, or approved. This is the master tracker for migration progress. See "Sitemap Maintenance" section. - Keep sitemap blocks[] current after every content change - After running import scripts, re-importing pages, or changing page content, immediately update the affected page's
blocks[]andsectionStyles[]in/sitemap.json. Before refactoring block CSS/JS, query the sitemap to find all affected pages and verify changes on them. - NEVER allow
.html(non-.plain.html) or.mdfiles in the content area - The/content/directory must ONLY contain.plain.htmlfiles. If you find any.html(that aren't.plain.html) or.mdfiles in the content area, delete them immediately. Import scripts must only produce.plain.htmloutput. This is non-negotiable. - Parser-first content workflow (NEVER edit
.plain.htmldirectly as first resort) - Content changes MUST go through the import pipeline: update parsers intools/importer/parsers/→ re-bundle → re-import. Direct.plain.htmledits are a LAST RESORT only. If you do edit.plain.htmldirectly, you MUST immediately update all impacted parsers to match and verify via a test re-import. Failure to do this causes parser/content drift that breaks future imports. - Check sitemap before modifying ANY parser - Before changing a parser, query
/sitemap.jsonto identify ALL pages that use the affected block. If any of those pages are already validated (importValidated: true) or approved (approved: true), the parser change could regress them. Flag this to the user before proceeding. - Assess cross-page re-import needs after block/style changes - When modifying a block's CSS/JS or a parser, check the sitemap for ALL other pages using that block or section style. Assess whether those pages need re-importing and/or re-validation. If the change is structural (parser output changed), re-import is mandatory for all affected pages. If the change is cosmetic (CSS-only), re-validation may suffice.
- Always update sitemap blocks[] and sectionStyles[] after EVERY import - After running import scripts on any page (new import or re-import), immediately update that page's
blocks[]andsectionStyles[]arrays in/sitemap.jsonto reflect the actual content produced. This is not optional — it must happen as part of every import operation, without exception.
IMPORTANT: When importing new pages or content, ALWAYS prioritize reusing existing blocks and their variants.
- Check the Block Reference table - Review all existing blocks and their variants
- Analyze if existing blocks can work - Consider:
- Can an existing block handle this content with its current structure?
- Can an existing variant be used with minor CSS adjustments?
- Can a new variant of an existing block solve the need?
- Only create new blocks when:
- No existing block can reasonably accommodate the content pattern
- The content structure is fundamentally different from all existing blocks
- Creating a variant would require more than 50% new code
New content section identified
↓
Does it match an existing block's purpose?
├─ YES → Use that block
│ ↓
│ Does styling match an existing variant?
│ ├─ YES → Use existing variant
│ └─ NO → Can styling be achieved with section styles (highlight)?
│ ├─ YES → Use base block + section style
│ └─ NO → Create new VARIANT (not new block)
│
└─ NO → Is it similar to any existing block?
├─ YES → Create new VARIANT of that block
└─ NO → Create new BLOCK (document it immediately!)
Use descriptive kebab-case names. Block-specific variants must be prefixed with the block name to avoid ambiguity. Generic variants that apply to any block or section use a short standalone name.
Block-specific (prefix with block name):
carousel-hero- Hero-style carousel (only makes sense on the carousel block)cards-featured- Featured card layout (specific to cards)columns-stats- Statistics display (specific to columns)carousel-testimonials- Quote carousel (specific to carousel)
Generic (no prefix, reusable across blocks/sections):
highlight- Light grey background
| Content Need | ✅ Correct Approach | ❌ Wrong Approach |
|---|---|---|
| Hero banner with different colors | Use carousel (carousel-hero) or teaser (teaser-hero) |
Create new hero-banner block |
| Single hero item (no rotation) | Use teaser (teaser-hero) |
Use carousel (carousel-hero) with single slide |
| Stats display (numbers + labels) | Use columns (columns-stats) |
Create new stats block |
| Expandable FAQ | Use accordion |
Create new faq block |
| Card grid | Use cards (default) |
Create new custom cards block |
| Logo strip | Use columns (columns-logos) |
Create new logo-strip block |
| Tabbed content | Use tabs |
Create new tabbed-content block |
| Two-column with image | Use columns (columns-media) variant |
Create new image-text block |
| Multiple two-column rows | Single columns (columns-media) with multiple rows |
Separate blocks for each row |
| Quote carousel | Use carousel (carousel-testimonials) variant |
Create new testimonials block |
When scraping pages with Playwright for migration, resize the browser before navigating:
// Set wide desktop viewport BEFORE navigating
await browser_resize({ width: 1440, height: 900 });
await browser_navigate({ url: 'https://example.com/page' });Why this matters:
- Responsive images serve higher resolution sources at wider viewports (
srcset,<picture>sources) - Background images may change based on viewport (desktop vs tablet vs mobile)
- Some content (e.g., "Show More" sections, mega menus) is only visible at desktop widths
- CSS
display: nonemay hide content at smaller breakpoints, causing missing data during extraction <picture>elements withmediaqueries serve different image URLs per breakpoint — wide viewport ensures the highest-quality source is selected
Applies to: All Playwright-based content extraction including browser_evaluate, browser_snapshot, browser_take_screenshot, and any scraper scripts.
When encountering a content pattern that's similar to an existing block:
- Identify the closest existing block (e.g., carousel, columns, cards)
- Analyze what's different (layout direction, styling, content structure)
- Create a variant by adding a class modifier (e.g.,
carousel-testimonials,columns-stats) - Add variant CSS in the same block's CSS file
- Update JS if needed to handle variant-specific decoration
Variant naming: Block-specific variants are prefixed with the block name. In authoring: | Carousel (carousel-testimonials) |
- This creates class:
.carousel.carousel-testimonials - Generic variants (e.g.,
highlight) are not prefixed and can be applied to any block or section
When to create a NEW block instead of variant:
- Content structure is fundamentally different (>50% different markup)
- JavaScript logic is completely different
- No shared styling or behavior with existing blocks
.plain.html content structure.
.plain.html files directly as a first approach. Instead: update parsers → re-bundle → re-import. Direct content edits create parser/content drift that silently breaks future imports and causes regressions across all pages using the same parsers.
The import scripts in tools/importer/ produce .plain.html files (div format). When the content structure changes (e.g., CSS-handled styling replaces inline markup), the parsers must be updated to match.
Rules for keeping scripts aligned:
- Content
.plain.htmlis the source of truth — If the content uses plain<p>for eyebrows, parsers must output plain<p>(not<p><strong>...</strong></p>). - CSS handles presentation — Bold, uppercase, colors, and spacing are all CSS concerns. Parsers should output clean semantic HTML and let block CSS handle visual styling.
- Create clean DOM elements in parsers — Always use
document.createElement()to build output elements rather than pushing source DOM nodes directly. Source nodes carry classes, attributes, and inline styles from the original site that don't belong in EDS content. - Verify after content changes — When modifying content markup patterns (e.g., removing
<strong>wrappers from eyebrows), update ALL parsers that produce that pattern. Search acrosstools/importer/parsers/for the old pattern. - Never overwrite verified content — When simulating an import to check alignment, compare the parser output against existing content HTML. Fix the parser to match the content, never the other way around.
- Use DOM-walking for flexible page imports — Parsers call
element.replaceWith(blockDiv), which detaches the original element. After all parsers run, walk the DOM to collect block<div>elements and remaining headings/paragraphs (default content) in natural document order. Group into sections. This approach handles pages with different block orders using the same import script. Seeimport-universal.jsfor the reference implementation. - If you MUST edit
.plain.htmldirectly — update parsers immediately after — Direct edits are a last resort (e.g., one-off content fix that doesn't apply to other pages). After ANY direct edit, you MUST: (a) update all impacted parsers intools/importer/parsers/to produce the same output, (b) re-bundle the import script, and (c) run a test import to verify the parser now matches the edited content. If you skip this step, the next re-import will overwrite your manual changes. - Check sitemap for cross-page impact before modifying parsers — Before changing any parser, query
/sitemap.jsonto find ALL pages using the affected block. If pages are alreadyimportValidated: trueorapproved: true, warn the user that the parser change may require re-importing and re-validating those pages. Never silently modify a parser used by validated pages.
Eyebrow text pattern (established):
- Content HTML:
<p>Eyebrow Text</p>(plain paragraph) - CSS:
font-weight: 700; text-transform: uppercase; letter-spacing: 1.6px;on the eyebrow class - Parser:
const p = document.createElement('p'); p.textContent = eyebrow.textContent.trim(); - Applies to: columns-feature, cards-awards, cards-stories, hero-featured eyebrows
Yellow accent segment pattern (established):
- Eyebrow dashes:
::beforepseudo-element,width: 32px; height: 3px; background: #ffd100; border-radius: 5px;positioned absolutely withleft: 0; top: 50%; transform: translateY(-50%);andpadding-left: 40pxon the text element - Heading bars:
::afterpseudo-element on H1,width: 80px; height: 4px; background: #ffdc40; border-radius: 5px; margin: 32px auto 0;displayed as block
All content files use .plain.html (div format). This is the native format that DA produces and consumes.
Checklist for every content .plain.html file:
-
Blocks as
<div class="block-name">elements- Block name as CSS class (lowercase, kebab-case)
- Each row is a
<div>child, each column within a row is a nested<div> - Variants:
<div class="block-name variant-name">
<!-- Single-column block --> <div class="article-header"> <div><div><h1>Title</h1></div></div> </div> <!-- Multi-column block --> <div class="cards-stories"> <div><div><picture>...</picture></div><div><h3>Title</h3></div></div> <div><div><picture>...</picture></div><div><h3>Title</h3></div></div> </div>
-
Sections as top-level
<div>wrappers (no<hr>separators)<div> <!-- Section 1 content --> </div> <div> <!-- Section 2 content --> <div class="section-metadata"> <div><div>Style</div><div>highlight</div></div> </div> </div>
-
Page metadata as
<div class="metadata">at the end<div class="metadata"> <div><div>Title</div><div>Page Title</div></div> <div><div>Description</div><div>Page description</div></div> <div><div>Image</div><div><picture>...</picture></div></div> <div><div>nav</div><div>/content/nav</div></div> <div><div>footer</div><div>/content/footer</div></div> </div>
-
No page shell — no
<!DOCTYPE>, no<html>, no<head>, no<body>, no<header>, no<footer>
Data tables: In .plain.html format, all tables are converted to divs. Data tables (non-block tables) should be handled through block implementations or other means.
This project follows the AEM Edge Delivery Services architecture where content and code are strictly separated:
- Code (JS, CSS, config): Lives in Git (
github.com/gabrielwalt/up), deployed via AEM Code Sync - Content (HTML pages, fragments): Lives in DA (Document Authoring at
content.da.live/gabrielwalt/up/), previewed/published via AEM admin API
Rules:
- Never push HTML content via Git — The
.gitignorehas*.htmlfor a reason - Never modify
.gitignoreto track HTML files — Content belongs in the CMS, not in the repo - Fragment content (nav, footer) comes from DA — These are authored and previewed in DA, not committed to Git
- Local
/content/directory is for local dev only — It mirrors DA content for local preview but is NOT tracked in Git - ZERO tolerance for
.html(non-.plain.html) or.mdfiles in/content/— The content area must ONLY contain.plain.htmlfiles. If you encounter any.htmlfiles (that aren't.plain.html) or.mdfiles in the content directory tree, delete them immediately. Import scripts must NEVER produce these formats. This rule has no exceptions.
.plain.html is the single source of truth. All content edits and updates are made directly to .plain.html files in the /content/ folder. No .html (non-.plain.html) or .md files may exist anywhere in the content folder tree — if found, they must be deleted immediately. Only .plain.html files are valid content.
.plain.html format (what you edit and what DA consumes):
- Section
<div>wrappers with content - Blocks as
<div class="block-name">(div format, not table format) - Includes a
<div class="metadata">block with page metadata (title, description, og:image, template) - No page shell, no
<head>, no<hr>separators, no<header>, no<footer>
Example .plain.html structure:
<div>
<h1>Page Title</h1>
<p>Introduction text.</p>
</div>
<div>
<div class="cards-stories">
<div><div><picture>...</picture></div><div><p>Eyebrow</p><h3>Title</h3></div></div>
<div><div><picture>...</picture></div><div><p>Eyebrow</p><h3>Title</h3></div></div>
</div>
<div class="section-metadata">
<div><div>Style</div><div>highlight</div></div>
</div>
</div>
<div class="metadata">
<div><div>Title</div><div>Page Title</div></div>
<div><div>Description</div><div>Page description</div></div>
<div><div>Image</div><div><picture>...</picture></div></div>
</div>Key rules:
- Blocks use div format:
<div class="block-name">with nested<div>rows and columns - Sections are
<div>wrappers at the top level (no<hr>separators needed) - Section metadata is a
<div class="section-metadata">inside its section<div> - Page metadata goes in a
<div class="metadata">at the end - No full page shell (
<!DOCTYPE>,<head>,<body>) — just the content divs
The header and footer blocks load their content as fragments via loadFragment():
header.jsfetches{navPath}.plain.html(default:/nav)footer.jsfetches{footerPath}.plain.html(default:/footer)- The
.plain.htmlformat is the standard content format
Fragment files are .plain.html like all other content. nav.plain.html and footer.plain.html exist on disk in /content/ and are edited directly.
Path resolution on deployed vs local:
- Deployed (aem.page/aem.live): Content at root paths —
/nav.plain.html,/footer.plain.html - Local dev (localhost:3000): Content at
/content/nav.plain.html(page metadata overrides the default path)
DA (Document Authoring) wraps inline content in <p> tags in its output. This is standard DA behavior and must NOT be worked around by unwrapping in JS.
Example — nav link in DA output:
<li>
<p><a href="https://about.ups.com/us/en/our-stories.html">Our Stories</a></p>
<ul>
<li><a href="...">Customer First</a></li>
</ul>
</li>Impact: EDS decorateButtons() in scripts.js finds links that are sole children of <p> and applies .button class + .button-wrapper on the parent <p>. This turns nav links into styled buttons.
Correct fix — CSS resets + flexible selectors:
/* Reset button styling applied by decorateButtons() */
header nav .nav-sections a.button:any-link {
display: inline;
margin: 0;
border: none;
padding: 0;
background: none;
color: currentcolor;
}
header nav .nav-sections .button-wrapper {
all: unset;
}/* JS selectors must match both patterns */
navSection.querySelector(':scope > a, :scope > p > a'); // ✓ handles both
navSection.querySelector(':scope > a'); // ✗ misses DA markupWrong approaches (do NOT use):
- ❌ JS code to unwrap
<p>tags around links — fights the CMS output - ❌ Copying
.plain.htmlfiles into the Git repo — breaks content/code separation - ❌ Modifying
.gitignoreto track HTML files — same issue
The user handles all Git operations. Do not:
- Run
git commit— leave changes unstaged for the user - Run
git push— the user pushes when ready - Run
git reset --hardor other destructive operations - Modify
.gitignorewithout explicit user approval
When code changes are complete, inform the user which files were modified so they can review, commit, and push.
This file is the project's source of truth. Keep it current to ensure consistency.
| Event | Required Updates |
|---|---|
| New block created | Add to Block Reference table, add full documentation in Custom Blocks section |
| New variant added | Update the block's variant table, document specifics |
| Block deleted | Remove from Block Reference, remove documentation |
| Variant removed | Update variant table, remove variant-specific docs |
| New section style | Add to Section Styles table |
| New page template | Add to Page Templates section |
| New design token | Add to Design Tokens table |
| New icon added | Add to Local Assets section |
| CSS pattern discovered | Add to CSS Patterns to Maintain |
| Bug fix with learnings | Add to Reminders section |
| Page imported/re-imported/refactored | Update /sitemap.json (see Sitemap Maintenance) |
| New page discovered on source site | Add to /sitemap.json with imported: false |
When creating a new block, document ALL of the following:
### block-name
**Location**: `/blocks/block-name/`
| Variant | Class | Purpose |
|---------|-------|---------|
| Default | `.block-name` | Description |
| block-name-variant | `.block-name.block-name-variant` | Block-specific description |
| generic-variant | `.block-name.generic-variant` | Generic style (reusable across blocks) |
**Authoring:**
\`\`\`
| Block Name (variant) |
| -------------------- |
| Content structure... |
\`\`\`
**Features**:
- Feature 1
- Feature 2
**Responsive behavior**:
- Mobile: ...
- Desktop: ...When adding a variant to an existing block:
- Add row to block's variant table
- Add "Variant-name specifics" section with:
- Key visual differences
- Unique behaviors
- Responsive changes
- CSS class name
When working on this project, periodically verify:
- All blocks in
/blocks/are documented - All variants mentioned in CSS are documented
- Design tokens match what's in
styles.css - Reminders section captures recent learnings
- Global styles:
/styles/styles.css - Lazy styles:
/styles/lazy-styles.css(post-LCP styles: scroll animations, arc section styles) - Delayed JS:
/scripts/delayed.js(IntersectionObserver for scroll animations) - Blocks:
/blocks/(all block directories listed in Block Reference) - Icons:
/icons/(search.svg,ups-logo.svg) - Icon font:
/fonts/upspricons.woff— UPS icon font (button chevron\e60f, circle arrow\e603) - Web fonts:
/fonts/(roboto-regular.woff2,roboto-medium.woff2,roboto-bold.woff2,roboto-condensed-bold.woff2) - Navigation: Fragment at
/content/nav.plain.html— loaded by header block vialoadFragment() - Footer: Fragment at
/content/footer.plain.html— loaded by footer block vialoadFragment() - Import infrastructure:
/tools/importer/(page-templates.json, parsers/, transformers/) - Sitemap:
/sitemap.json— Master tracker for all pages, import status, block usage, validation, and approval state. Must be kept up-to-date at all times (see Sitemap Maintenance section).
/sitemap.json is the master tracker for the entire migration. It must always reflect the current state of every page, fragment, and block in the project.
| Event | Required Update |
|---|---|
| New page discovered on original site | Add entry to pages[] with sourceUrl, imported: false |
| Page imported (content created) | Set imported: true, populate blocks[] and sectionStyles[] — MANDATORY, not optional |
| Import re-run on existing page | Update blocks[] and sectionStyles[] to match new content — even if you think nothing changed |
| Import validated | Set importValidated: true |
| Page critiqued/approved | Set critiqued: true / approved: true on the page entry |
| Content refactored (blocks changed) | Update blocks[] to reflect current block composition |
| Section style added/removed | Update sectionStyles[] to match current content |
| Page removed | Remove the entry from pages[] |
| New fragment created | Add entry to fragments[] |
| Parser modified | Check ALL pages using that parser's block — reset importValidated to false on affected pages if their content may have changed |
{
"fragments": [
{ "path": "/nav", "imported": true, "importValidated": true, "critiqued": true, "approved": true }
],
"pages": [
{
"path": "/us/en/page-name",
"sourceUrl": "https://about.ups.com/us/en/page-name.html",
"imported": true,
"importValidated": false,
"critiqued": false,
"approved": false,
"blocks": ["block-name", "another-block"],
"sectionStyles": ["style-name", "another-style"]
}
]
}- Always update after any content change — If blocks are added, removed, or restructured on a page, update the corresponding
blocks[]array immediately. - Don't mark validated/critiqued/approved prematurely — These flags live at the page level (not on individual blocks or styles). Only set them after the corresponding step is actually completed and verified.
- Paths use no extension — Page paths are stored without
.html(e.g.,/us/en/home, not/us/en/home.html). - Source URLs are the original site URLs — Always include the full
https://about.ups.com/...URL. - Keep blocks[] populated for ALL pages —
blocks[]andsectionStyles[]are simple string arrays (e.g.,["cards-stories", "hero-featured"]). Every page must have these arrays reflecting the actual blocks and section styles present in the content. Pages with no blocks should haveblocks: []. - Update blocks[] after every content operation — After running import scripts, re-importing pages, or executing any user request that changes page content (adding/removing blocks, changing section styles), immediately update the affected page's
blocks[]andsectionStyles[]arrays to match the new content. - Use blocks[] for impact analysis before ANY parser or block change — Before modifying a parser, block CSS/JS, or section style, query the sitemap to find ALL pages that use that block or style. This is MANDATORY, not optional. If any affected pages have
importValidated: trueorapproved: true, you MUST warn the user before proceeding and assess whether those pages need re-importing and re-validation. Example: changing thecards-storiesparser requires checking every page wherecards-storiesappears inblocks[]. - Assess re-import needs after structural changes — When a parser change alters the output HTML structure (not just cosmetic fixes), ALL pages using that parser's block MUST be re-imported to stay consistent. Reset
importValidated: falseon those pages. For CSS-only changes, re-validation (visual check) may suffice without re-import, but flag the affected pages to the user regardless. - Sitemap is the safety net for cross-page regressions — The sitemap exists specifically to prevent blind changes that break already-validated pages. Treat it as a pre-flight checklist: check it BEFORE making changes, update it AFTER making changes. Never skip this discipline.
All page and fragment tracking is in /sitemap.json. Do not duplicate page listings here — the sitemap is the single source of truth for all pages, their source URLs, import status, block usage, section styles, and validation state. See "Sitemap Maintenance" section for the full schema and update rules.
URL mapping convention: Local paths follow the origin URL structure with /content/ prefix. All content files use .plain.html extension. Paths in sitemap.json omit the extension (e.g., /us/en/home).
When asked to list all page URLs (e.g., "list all pages", "bulk publish", "bulk preview"):
- Read
/sitemap.jsonand iterate over allfragments[]andpages[]entries - For each entry, use its
pathfield to construct the URL - Output each as
https://main--up--gabrielwalt.aem.page/{path} - Include ALL pages and fragments
- Output the URLs inside a fenced code block — one per line, no headers, no extra text
Fragment files (nav, footer) are .plain.html like all other content. They are authored in DA and loaded by blocks at runtime via loadFragment(). They are NOT committed to Git.
Content source: DA at content.da.live/gabrielwalt/up/
Deployed paths: /nav.plain.html, /footer.plain.html (served by AEM)
Local dev paths: /content/nav.plain.html, /content/footer.plain.html (for local preview only)
Fragment .plain.html format: Same div-format as all other content files — section <div> wrappers with block <div class="block-name"> elements. No page shell, no <header> or <footer> tags.
DA markup note: DA wraps inline content in <p> tags. Block CSS/JS must handle this — see "DA Markup Compatibility" in Content Architecture section.
Defined in /styles/styles.css — reference these variable names, don't hardcode values.
| Variable | Value | Usage |
|---|---|---|
--background-color |
#fff |
Page and card backgrounds |
--light-color |
#f2f2f2 |
Highlight section backgrounds, dividers |
--dark-color |
#505050 |
Secondary text, disabled states |
--text-color |
#242424 |
Primary body and heading text |
--link-color |
#426da9 |
Links, default button borders |
--link-hover-color |
#244674 |
Link/button hover states |
| Variable | Value | Usage |
|---|---|---|
--color-gold |
#ffc400 |
Brand gold — CTA backgrounds, accent dashes, heading bars |
--color-gold-hover |
#e0ac00 |
Hover-darkened gold for CTA buttons |
| Variable | Value | Usage |
|---|---|---|
--color-muted |
#767676 |
Muted grey text (attribution, submenu labels, accent-bar h6) |
--color-border |
#e5e5e5 |
Separator/border grey (header pipes, dividers) |
| Variable | Value | Usage |
|---|---|---|
--spacing-xs |
8px |
Tight spacing |
--spacing-s |
16px |
Small spacing |
--spacing-m |
24px |
Medium spacing (block gap, card gaps) |
--spacing-l |
32px |
Large spacing |
--spacing-xl |
40px |
Card/component padding, eyebrow offset |
--spacing-2xl |
48px |
Desktop card padding |
--spacing-3xl |
64px |
Nav/footer edge spacing |
--spacing-4xl |
80px |
Quadruple extra-large |
One token controls internal default-content spacing. All other spacing uses the spacing scale tokens directly.
| Variable | Value | Purpose |
|---|---|---|
--block-gap |
24px (--spacing-m) |
Gap between elements within a wrapper (* + * rule) |
Note: --section-padding has been removed. The spacing system is now margin-driven — see "Vertical Spacing Rules" below.
| Variable | Value | Usage |
|---|---|---|
--radius-s |
4px |
Small radius (navigation-tabs links) |
--radius-m |
8px |
Medium radius (cards, content cards, stats card) |
--radius-l |
16px |
Large radius (dropdown menus, stats container) |
--radius-pill |
80px |
Pill shape (buttons) |
| Variable | Value | Usage |
|---|---|---|
--shadow-card |
0 4px 24px rgb(0 0 0 / 16%) |
Card resting state |
--shadow-card-hover |
0 8px 32px rgb(0 0 0 / 18%) |
Card hover state |
--shadow-dropdown |
0 8px 16px rgb(0 0 0 / 8%) |
Mega menu dropdown |
| Variable | Mobile | Desktop (≥992px) |
|---|---|---|
--content-max-width |
1200px |
same |
--content-padding |
24px (--spacing-m) |
32px (--spacing-l) |
| Variable | Value | Usage |
|---|---|---|
--cta-bg |
var(--color-gold) |
Gold CTA button background |
--cta-bg-hover |
var(--color-gold-hover) |
Gold CTA button hover |
--cta-text |
#121212 |
CTA button text color |
| Variable | Value | Usage |
|---|---|---|
--eyebrow-size |
var(--body-font-size-xs) (13px) |
Eyebrow font size |
--eyebrow-weight |
700 |
Eyebrow font weight |
--eyebrow-tracking |
1.6px |
Eyebrow letter spacing |
| Variable | Value | Usage |
|---|---|---|
--accent-dash-width |
32px |
Eyebrow yellow dash width |
--accent-dash-height |
3px |
Eyebrow yellow dash height |
--accent-dash-color |
var(--color-gold) |
Eyebrow yellow dash color |
| Variable | Mobile | Desktop (≥992px) |
|---|---|---|
--body-font-family |
roboto, roboto-fallback, sans-serif |
same |
--heading-font-family |
roboto, roboto-fallback, sans-serif |
same |
--body-font-size-m |
16px |
same |
--body-font-size-s |
14px |
same |
--body-font-size-xs |
13px |
same |
--heading-font-size-xxl |
64px |
64px |
--heading-font-size-xl |
40px |
40px |
--heading-font-size-l |
24px |
32px |
--heading-font-size-m |
20px |
24px |
--heading-font-size-s |
18px |
20px |
--heading-font-size-xs |
16px |
16px |
| Variable | Value | Usage |
|---|---|---|
--viewport-desktop |
992px |
Desktop layout breakpoint (grid, multi-column) |
--viewport-nav |
1024px |
Navigation breakpoint (hamburger → full nav) |
Note: CSS custom properties cannot be used inside @media conditions — media queries are evaluated before the cascade. These tokens are reference values: use the literal 992px / 1024px in media queries and annotate with a /* --viewport-desktop */ or /* --viewport-nav */ comment above each @media rule.
| Variable | Mobile | Desktop (≥1024px) |
|---|---|---|
--nav-height |
64px |
104px |
NEVER use these incorrect variable names:
→ Use--spacing-sm--spacing-s→ Use--spacing-md--spacing-m→ Use--spacing-lg--spacing-l
Before writing any CSS property with var(--...), cross-check against the variables defined in styles.css. If the variable isn't defined, it does NOT exist and will silently fail.
Spacing is margin-driven — wrappers carry margin-top, and sections have no padding by default so wrapper margins collapse through section boundaries for cross-section gaps.
Key principle: Sections with padding: 0 are transparent to margin collapsing. A block wrapper's 80px margin-top will collapse through the section boundary, creating the same gap whether the next element is in the same section or a different one.
Gap hierarchy:
| Scenario | Gap | Token |
|---|---|---|
| Block wrapper → Block wrapper | 80px | --spacing-4xl |
| Block wrapper ↔ Default-content (same section) | 32px | --spacing-l |
| Default-content (cross-section base) | 40px | --spacing-xl |
| Default-content → Default-content (same section) | 24px | --spacing-m |
Elements within any wrapper (* + *) |
24px | --block-gap |
| H1 element (above) | 80px | --spacing-4xl |
| Nav → first content (H1) | 80px | H1 margin collapses through section |
| Last section → footer | 80px | padding-bottom on last section |
| Before background section (highlight) | 80px | margin-top on the section |
| Background section padding (highlight) | 80px | --spacing-4xl top and bottom |
CSS selector summary:
main > .section → padding: 0
main > .section:last-of-type → padding-bottom: 80px
main > .section > div:not(.default-content-wrapper) → margin-top: 80px (block wrappers)
main > .section > .default-content-wrapper → margin-top: 40px (cross-section base)
…+ .default-content-wrapper (after block) → margin-top: 32px (same-section override)
.default-content-wrapper + div:not(…) → margin-top: 32px (same-section override)
.default-content-wrapper + .default-content-wrapper → margin-top: 24px (same-section override)
main > .section > div > * + * → margin-top: 24px (internal gap)
main .default-content-wrapper > h1 → margin-top: 80px
main > .section.highlight → margin-top: 80px; padding: 80px 0
> div:first-child → margin-top: 0
> div:last-child → margin-bottom: 0Rules:
- Sections have NO padding by default — this is critical for margin collapsing through sections.
- Block wrappers get 80px margin-top — this is the primary inter-block gap.
- Default-content wrappers get 40px margin-top — overridden to 32px when adjacent to a block wrapper (same section), 24px when adjacent to another default-content-wrapper.
- H1 gets 80px margin-top — collapses with wrapper margin for consistent nav-to-H1 gap.
- 24px between elements within wrappers —
main > .section > div > * + *applies 24px gap. - No section margins on regular sections — regular sections have
padding: 0andmargin: 0. - Background sections (highlight) use
margin-top: 80px(white space before background) +padding: 80px 0(internal spacing) with first/last child margin reset to 0. This creates symmetric 80px white + 80px colored padding on both entering and exiting the background zone. - Last section gets
padding-bottom: 80pxfor footer gap. p.button-wrapperhasmargin: 0— spacing is handled by the* + *rule; extra margin would leak through sections.- Blocks must not set outer margins on their wrapper — the global spacing system handles all inter-block spacing.
- Never use
!important- increase selector specificity instead - Use CSS custom properties - reference design tokens, override at block level when needed
- Edge-to-edge blocks - use
:has()selector on wrapper:main > div:has(.block-name) - Specificity order in styles.css - section-specific styles must come BEFORE template styles to maintain proper cascade
- Visually hidden text - use
clip-path: inset(50%)instead of deprecatedclip: rect() - Backdrop filter - always include both
-webkit-backdrop-filterandbackdrop-filter - Never write selectors that depend on sibling element sequences - Selectors like
h3 + h3 + p > strongare fragile and break when an author reorders, adds, or removes content. If a style only works when exactly the right sequence of elements exists on the page, it is un-authorable. Authors cannot be expected to know that adding a heading before a paragraph will change its styling. Always prefer: (a) inline markup the author controls (e.g.,<strong>), (b) block or section variants with explicit class names, or (c) accepting a "good enough" approximation over a fragile pixel-perfect hack. Better to have a slightly imperfect style than an unmaintainable one.
- no-descending-specificity: For complex block CSS with variant overrides, add
/* stylelint-disable no-descending-specificity */at the top of the file - declaration-block-no-duplicate-properties: Never duplicate CSS properties (except vendor prefixes like
-webkit-) - property-no-deprecated: Use modern equivalents (
clip-pathnotclip)
Only two breakpoints, derived from the UPS source site. Content flows fluidly between them — avoid adding extra breakpoints.
| Breakpoint | Token | Value | Usage |
|---|---|---|---|
| desktop | --viewport-desktop |
992px |
Below: single-column mobile layout. Above: multi-column desktop layout. |
| nav | --viewport-nav |
1024px |
Below: hamburger menu. Above: full horizontal navigation. |
Content max-width: 1200px — main content sections are capped at this width and centered.
Media query syntax (use modern CSS syntax, annotate with token name comment):
/* --viewport-desktop */
@media (width >= 992px) { }
/* --viewport-desktop */
@media (width < 992px) { }
/* --viewport-nav */
@media (width >= 1024px) { }Principles:
- Content max-width of 1200px - Main content is constrained and centered; header, footer, and edge-to-edge blocks may extend to full viewport width
- Use percentage-based or viewport-relative widths - Prefer
%,vw,frunits over fixedpxwidths for containers - Flexible grids with auto-fill - Use
repeat(auto-fill, minmax(min, 1fr))for responsive card layouts - Smooth transitions - When switching layouts at breakpoints, ensure visual continuity
- Two breakpoints only - Resist adding intermediate breakpoints; let content reflow naturally
- Link → Button: Link alone in its own paragraph becomes a button
- Link stays link: Link inline with other text stays a link
- Section metadata: Use
section-metadatablock to apply styles likehighlight,accent-bar - Page templates: Add
Template: template-nameto page metadata for page-specific styles - HTML in table cells: Markdown syntax (like
## Heading) is NOT parsed inside table cells. Use HTML tags (<h2>Heading</h2>) when you need structured content in block tables. - One row per item: In block tables (carousel, accordion), each row becomes one item/slide. Combine all content for an item into a single row using HTML.
- Data tables vs block tables: In EDS,
<table>elements are converted to blocks byconvertBlockTables()in scripts.js. To include an actual data table (not a block), ensure the first cell of the first row is empty or contains multi-word data text (not a valid block name). TheconvertBlockTables()function checks the first cell — iftoClassName()returns an empty string, the table passes through as a native HTML table. Use<th>for header cells and<td>for data cells. Style data tables instyles.cssundermain .default-content-wrapper table.
Templates are applied via page metadata: Template: template-name
| Template | Class Applied | Purpose |
|---|---|---|
| (none defined yet) |
Default content (text, headings, buttons, images in .default-content-wrapper) should be centered on all pages.
CSS selector pattern:
main .default-content-wrapper {
text-align: center;
}Applied via section-metadata block with Style: style-name. Multiple styles can be combined.
| Style | Class | Purpose |
|---|---|---|
highlight |
.section.highlight |
Light grey background (--light-color) |
accent-bar |
.section.accent-bar |
Adds yellow bar under h1/h2 (::after), uppercase h6 eyebrow |
arc |
.section.arc |
Warm grey gradient background with white curved scoop at bottom (decorative SVG ::after) |
arc-wave |
.section.arc-wave |
Flat grey background with organic white wave at bottom — the "inverted arc" (decorative SVG ::after) |
arc-gradient |
.section.arc-gradient |
Subtle warm beige gradient wash behind content (decorative SVG ::after, no background color change) |
dark |
.section.dark |
Dark brown background (#351c15), inverts text/links to white |
spacing-l |
.section.spacing-l |
Adds 80px (--spacing-4xl) margin-top to section |
spacing-xl |
.section.spacing-xl |
Adds 160px margin-top to section |
spacing-2xl |
.section.spacing-2xl |
Adds 240px margin-top to section |
Example usage in content:
<div class="section-metadata">
<div><div>Style</div><div>highlight</div></div>
</div>Decorative SVG arc backgrounds from the original UPS site. These create curved transitions between sections using ::after pseudo-elements with inline SVG data URIs, positioned behind content at z-index: -1. CSS is in /styles/lazy-styles.css.
arc — Grey gradient section with white curved bottom edge:
- Background:
linear-gradient(318.8deg, #DFDBD7, #F2F1EF)(warm grey-to-lighter) ::after: White concave SVG scoop (1440x72 viewBox) at bottom of section- Spacing:
margin: 80px 0 -215px,padding: 80px 0 215px— negative bottom margin lets next section overlap into curve area - SVG height is responsive:
padding-top: calc(5%)scales with viewport width - Next section gets
position: relative; z-index: 1to render above the curve - Used on: our-stories (H1 section with hero overlap below), our-company (H1 section with hero overlap below), category pages (customer-first, innovation-driven, people-led H1 sections)
arc-wave — Flat grey background with organic white wave at bottom (inverted arc):
- Background:
var(--light-color)(#f2f2f2) — flat grey, no gradient ::after: Organic/irregular white wave SVG (1381x118pt viewBox) overlapping upward behind content from section bottom- Spacing:
margin: 80px 0 0,padding: 80px 0 32px— no extra padding for the wave; SVG extends upward behind content - SVG height is responsive:
padding-top: calc(8.5%)scales with viewport width - Next section gets
margin-top: 32pxfor a tight transition - Used on: our-culture (intro section)
arc-gradient — Subtle warm beige wash (no visible background change):
- No background color on the section itself (stays white/transparent)
::after: Large gradient SVG (1440x560 viewBox) with curved bottom edge, fills from#DFDBD7(beige) to transparent#F2F1EF. Bottom-aligned.- No extra margin or padding — uses default section spacing
- Very subtle decorative effect; barely noticeable on white backgrounds
- Used on: home (hero+stories section), our-impact (columns-feature section)
Complete reference of all blocks and their variants.
| Block | Location | Variants | Description |
|---|---|---|---|
| header | /blocks/header/ |
— | Site header with logo, nav links, mega menu dropdowns, utility links |
| footer | /blocks/footer/ |
— | Site footer with multi-column links, legal links, copyright |
| fragment | /blocks/fragment/ |
— | Utility for loading content fragments |
| columns-feature | /blocks/columns-feature/ |
— | Two-column feature card with eyebrow, heading, CTA, image |
| columns-quote | /blocks/columns-quote/ |
— | Testimonial/quote with portrait image |
| columns-stats | /blocks/columns-stats/ |
— | Full-width image with overlapping stats panel |
| cards-awards | /blocks/cards-awards/ |
— | Text-only award cards with eyebrow and heading |
| cards-stories | /blocks/cards-stories/ |
— | Image + text story cards in a clickable grid |
| hero-featured | /blocks/hero-featured/ |
hero-featured-right |
Hero with background image and white card overlay |
| contact-card | /blocks/contact-card/ |
— | Contact info card with title, two columns, and vertical separator |
| navigation-tabs | /blocks/navigation-tabs/ |
navigation-tabs-inline |
Card-style or inline navigation links with active tab indicator |
| fact-sheets | /blocks/fact-sheets/ |
— | Responsive stat grid with icons, numbers, labels, and CTA |
| columns-media | /blocks/columns-media/ |
— | Asymmetric image + text (1/3 + 2/3), image on either side |
| breadcrumb | /blocks/breadcrumb/ |
— | Auto-generated breadcrumb from URL path segments |
| article-header | /blocks/article-header/ |
— | Story article header with eyebrow, title, byline, subtitle, hero image |
| embed | /blocks/embed/ |
— | YouTube video embed with responsive 16:9 aspect ratio |
| social-share | /blocks/social-share/ |
— | Social media share links (Facebook, Twitter, LinkedIn, Email) |
| cards-leadership | /blocks/cards-leadership/ |
— | Horizontal person cards with portrait, name, title in 2-col grid |
| cards-reports | /blocks/cards-reports/ |
cards-reports-text |
Horizontal document cards with thumbnail, title, action link |
| awards-list | /blocks/awards-list/ |
— | Year-tabbed list of award entries with eyebrow, title, meta |
| timeline | /blocks/timeline/ |
— | Vertical timeline with period nav sidebar and scroll spy |
| form | /blocks/form/ |
— | Styled form with text, email, textarea, select, submit fields |
| data-table | /blocks/data-table/ |
— | Converts div structure to native HTML <table> for data tables |
| leadership-bio | /blocks/leadership-bio/ |
— | Two-column bio: text (name, title, paragraphs) left, portrait right |
| investor-links | /blocks/investor-links/ |
— | Centered quick links (Email Alerts, Contacts) with icons |
Boilerplate blocks (vanilla, unmodified): cards, columns, hero
Location: /blocks/header/
Features:
- Logo from nav fragment (60px height)
- Horizontal nav links on desktop (flex layout, 32px gap)
- Full-width mega menu dropdowns (position: fixed, 100vw width, max-width 1200px inner content)
- Utility links (ups.com, Support) with pipe separators
- Hamburger menu on mobile (animated icon, full-height overlay)
- 2px bottom border (
var(--light-color)) - DA-compatible: CSS resets neutralize button styling on nav links, JS selectors handle both
> li > aand> li > p > apatterns
Fragment source: Nav content authored in DA at /nav, loaded via loadFragment('/nav')
Responsive behavior:
- Mobile (<1024px): Fixed position, hamburger menu, 64px height
- Desktop (≥1024px): Relative position, horizontal nav, 100px height
Dropdown behavior: Clicking a nav item with children toggles aria-expanded, showing a mega menu panel below the header. Chevron arrows indicate dropdown state. Each dropdown link has a right-pointing chevron arrow (::after).
Location: /blocks/footer/
Features:
- Top row: Highlighted links (Newsroom, Careers) with gold/yellow background strip
- Middle: Multi-column link grid (This Site, Other UPS Sites, Connect, Subscribe)
- Bottom: Legal links with pipe separators, copyright text
Location: /blocks/columns-feature/
| Variant | Class | Purpose |
|---|---|---|
| Default | .columns-feature |
Two-column feature card with eyebrow, heading, description, CTA, and image |
Authoring:
| Columns-Feature |
| --- | --- |
| <p>Eyebrow Text</p><h2>Heading</h2><p>Description</p><p><a href="...">CTA</a></p> | <picture>...</picture> |
Features:
- Eyebrow text (plain
<p>, CSS handles bold/uppercase), h2 heading, description paragraph, CTA link - Horizontal yellow accent dash (
::before) on eyebrow text - Image in one column, text content in the other
- Column order follows source (image left or right)
Responsive behavior:
- Mobile: stacks vertically, image on top
- Desktop (>=992px): side-by-side 50/50 columns
Location: /blocks/columns-quote/
| Variant | Class | Purpose |
|---|---|---|
| Default | .columns-quote |
Testimonial/quote with portrait image |
Authoring:
| Columns-Quote |
| --- | --- |
| <h3>"Quote text..."</h3><p>Attribution Name</p> | <picture>...</picture> |
Features:
- Quote text as h3, attribution name as plain
<p>(CSS handles bold/uppercase) - Portrait image in second column
Responsive behavior:
- Mobile: stacks vertically
- Desktop (>=992px): quote left, image right
Location: /blocks/cards-awards/
| Variant | Class | Purpose |
|---|---|---|
| Default | .cards-awards |
Text-only award cards with eyebrow and heading |
Authoring:
| Cards-Awards |
| --- |
| <p>Eyebrow Text</p><h3>Award description</h3> |
| <p>Eyebrow Text</p><h3>Award description</h3> |
Features:
- Text-only cards (no images)
- Eyebrow category label (plain
<p>, CSS handles bold/uppercase) + h3 heading per card - Grid layout with responsive columns
Responsive behavior:
- Mobile: single column
- Desktop: auto-fill grid (min 257px per card)
Location: /blocks/hero-featured/
| Variant | Class | Purpose |
|---|---|---|
| Default | .hero-featured |
Hero with background image and white card overlay (card on left) |
| hero-featured-right | .hero-featured.hero-featured-right |
Card positioned on right side of image |
Authoring:
| Hero-Featured |
| --- |
|  |
| <p>Eyebrow</p><h4>Heading</h4><p>Description</p><p><a href="...">CTA</a></p> |
Right variant:
| Hero-Featured (hero-featured-right) |
| --- |
|  |
| <p>Eyebrow</p><h4>Heading</h4><p>Description</p><p><a href="...">CTA</a></p> |
Features:
- Background image fills entire block (first row →
position: absolute; inset: 0, picture alsoposition: absolute; inset: 0, imgobject-fit: cover) - White card overlay (border-radius 8px, no box-shadow)
- Eyebrow text with horizontal yellow accent dash (
::before, 32x3px, #ffd100) - h4 heading, description, gold CTA button (#ffc400 bg)
- Equal spacing between image edge and card on all visible sides
- Supports both
<picture>and bare<img>for background image
hero-featured-right specifics:
- Card positioned on right side using
justify-content: flex-end(desktop) - Mirror of default left positioning:
margin: 0 60px 0 0instead of0 0 0 60px - Parser detects
.upspr-heroimage_content--rightclass in source DOM
Responsive behavior:
- Mobile: min-height 650px, card max-width 480px, padding 24px, margin
200px 24px 24px(equal left/bottom/right spacing of 24px) - Desktop (>=992px): card max-width 480px with
box-sizing: border-box, padding 48px, margin60px 0 60px 60px(left) or60px 60px 60px 0(right)
Location: /blocks/cards-stories/
| Variant | Class | Purpose |
|---|---|---|
| Default | .cards-stories |
Image + text story cards in a grid |
Authoring:
| Cards-Stories |
| --- | --- |
|  | <p>Eyebrow</p><h3>Title</h3><p>Description</p><p><a href="...">Link</a></p> |
|  | <p>Eyebrow</p><h3>Title</h3><p>Description</p><p><a href="...">Link</a></p> |
Features:
- Image + text cards with eyebrow category label and horizontal yellow accent dash (
::before, 32x3px) - Entire card is clickable (wraps in anchor)
- Image zoom on hover, box-shadow hover effect
- 16:10 aspect ratio images
Responsive behavior:
- Mobile: single column
- Desktop (>=992px): 3-column grid
Location: /blocks/columns-stats/
| Variant | Class | Purpose |
|---|---|---|
| Default | .columns-stats |
Full-width image background with overlapping stats panel |
Authoring:
| Columns-Stats |
| --- | --- |
|  | <h4>~460K</h4><p>Label</p><h4>200+</h4><p>Label</p>...<p><a href="...">CTA</a></p> |
Features:
- JS restructures DOM: image becomes absolute-positioned background, stats overlay on top
- Inner container with 16px border-radius, 1200px max-width, overflow hidden
- Background image fills entire block height at all breakpoints (
picture: position absolute, inset 0; img: object-fit cover) - Stats panel with white background, 8px border-radius
- Each stat: h4 number + p label pair, separated by 4px solid
var(--light-color)borders - Gold/yellow CTA button (
#ffc400bg,#121212text)
Responsive behavior:
- Mobile: Image fills full block height as background, stats card overlaid with
margin: 120px 24px 24px, padding24px 16px - Desktop (>=992px): Stats panel 280px wide,
margin: 30px 0 30px auto(right-aligned), padding24px 20px, imageborder-radius: 16px
Location: /blocks/navigation-tabs/
| Variant | Class | Purpose |
|---|---|---|
| Default | .navigation-tabs |
Card-style navigation links with arrow icons |
| navigation-tabs-inline | .navigation-tabs.navigation-tabs-inline |
Horizontal inline text links with active tab indicator |
Authoring (default - card style):
| Navigation-Tabs |
| --------------- |
| [Link Text](url) |
| [Link Text](url) |
Authoring (inline - text links):
| Navigation-Tabs (navigation-tabs-inline) |
| ---------------------------------------- |
| [Link Text](url) |
| [Link Text](url) |
Features:
- Default variant: Row of clickable cards with heading and right-arrow icon
- Inline variant: Horizontal text links, centered, with bottom border separator
- Active tab detection: JS normalizes current page path and compares against link paths, adding
navigation-tabs-activeclass - Active tab indicator: Gold
::beforepseudo-element (24px wide, 3px tall) centered under the active link - DA button reset: removes
.buttonand.button-wrapperclasses from links
Responsive behavior:
- Default: cards stack on mobile, horizontal on desktop
- Inline: horizontal row at all viewports, centered via flexbox
Location: /blocks/fact-sheets/
| Variant | Class | Purpose |
|---|---|---|
| Default | .fact-sheets |
Responsive stat grid with icons, numbers, labels, and gold CTA |
Authoring:
| Fact-Sheets |
| --- | --- |
| <img icon1> | <h4>~460K</h4><p>Employees</p> |
| <img icon2> | <h4>200+</h4><p>Countries & territories served</p> |
| <img icon3> | <h4>20.8M</h4><p>Packages delivered daily</p> |
| <img icon4> | <h4>$88.7B</h4><p>2025 Revenue</p> |
| <p><strong><a href="...">View All Fact Sheets</a></strong></p> |
Features:
- Each stat has its own icon (57x57px SVG), large number (h4), and label (p)
- JS restructures rows into a flex container with
.fact-sheets-itemwrappers (centered via flexbox) - Last row (link without h4) becomes gold CTA button below the grid
- Grey separators between items (4px solid
--light-color) - Center-aligned content
Responsive behavior:
- Mobile (<600px): 1 column, horizontal separators between items
- Tablet (600px-991px): 2 columns, vertical + horizontal separators
- Desktop (>=992px): 4 columns, vertical separators only
Location: /blocks/columns-media/
| Variant | Class | Purpose |
|---|---|---|
| Default | .columns-media |
Asymmetric two-column layout (~1/3 image + ~2/3 text), image on either side |
Authoring:
Image on the left (values sections):
| Columns-Media |
| --- | --- |
| <picture>image1</picture> | <h2>Heading</h2><p>Description with <strong>bold terms</strong>.</p><ul><li><strong>Term</strong>: explanation</li></ul> |
| <picture>image2</picture> | <h2>Heading</h2><p>Description.</p><p><strong>Sub-heading</strong><br>Additional text.</p> |
Text on the left, image on the right (intro/hero usage):
| Columns-Media |
| --- | --- |
| <h1>Heading</h1><p>Description text.</p> | <picture>image</picture> |
Features:
- Multi-row block — each row is one image + text item
- Supports both image-left and image-right layouts based on DOM order
- Image-left (first child): fixed 275px width on desktop, square aspect ratio
- Image-right (last child): flexible ~38% width, natural aspect ratio
- Text column: fluid width, h2 heading (font-weight 400), paragraphs, optional
<ul>bullet lists - Inline
<strong>for key terms within body text and list items - No card styling (no shadow, no border-radius, no background)
- No eyebrow, no CTA button, no yellow accent line
- 32px gap between image and text columns
- 32px vertical spacing between consecutive rows (
var(--spacing-l)) - Works well in
highlightsection for grey background intro areas
Responsive behavior (three-tier):
- Phone (<768px): single column stacked (DOM order preserved), max-width 320px centered, images max 290px, 24px top padding on text
- Tablet (≥768px): 50/50 two-column side-by-side, DOM order preserved, 32px gap
- Desktop (≥992px): asymmetric — image-left uses 275px fixed + text fluid; image-right uses ~38% flexible + text fluid. 32px gap, top-aligned
Location: /blocks/breadcrumb/
| Variant | Class | Purpose |
|---|---|---|
| Default | .breadcrumb |
Auto-generated breadcrumb trail from URL path |
Features:
- Auto-generates breadcrumb from URL path (Home / Segment / Current Page)
- Strips
/contentprefix and locale prefix (/us/en/) - Hidden on home page (removes its section entirely)
- Desktop only (hidden below 992px via section
display: none) - Accessible
<nav aria-label="Breadcrumb">with<ol>list - Angled slash separators between items (
::beforepseudo-element) - Current page shown as plain text (no link), intermediate segments linked
Responsive behavior:
- Mobile (<992px): hidden entirely (section
display: none) - Desktop (>=992px): horizontal breadcrumb trail, 32px below nav
Location: /blocks/fragment/
Note: Not typically used as a block in content. Provides loadFragment() utility function used by header.js and footer.js to load nav and footer content.
If used as a block:
| Fragment |
| -------- |
| /path/to/fragment |
Loads the referenced fragment HTML and inserts it into the page.
Location: /blocks/article-header/
| Variant | Class | Purpose |
|---|---|---|
| Default | .article-header |
Story article header with eyebrow link, title, byline, subtitle, hero image |
Authoring:
| Article-Header |
| -------------- |
| <a href="/category">Eyebrow Category</a> |
| <h1>Article Title</h1> |
| <p>03-04-2026 | 2 Min Read</p> |
| <p>Subtitle text</p> |
| <picture>hero image</picture> |
Features:
- 5-row block: eyebrow link, h1 title, byline (date + read time), subtitle, hero image
- Eyebrow link with horizontal yellow accent dash (
::before, 32x3px,--accent-dash-color) - DA button reset: eyebrow link gets
.buttonfromdecorateButtons(), CSS resets it - Byline in uppercase with letter-spacing
- Subtitle in italic
- Hero image full-width
Responsive behavior:
- All viewports: single column, left-aligned, max-width constrained by
--content-max-width
Location: /blocks/embed/
| Variant | Class | Purpose |
|---|---|---|
| Default | .embed |
YouTube video embed |
Authoring:
| Embed |
| ----- |
| <a href="https://www.youtube.com/watch?v=VIDEO_ID">https://www.youtube.com/watch?v=VIDEO_ID</a> |
Features:
- Extracts YouTube video ID from watch URL
- Creates responsive iframe with 16:9 aspect ratio (
padding-bottom: 56.25%) - Lazy loading, full accessibility attributes (allow autoplay, encrypted-media, etc.)
Responsive behavior:
- All viewports: fluid 16:9 container, 100% width
Location: /blocks/social-share/
| Variant | Class | Purpose |
|---|---|---|
| Default | .social-share |
Social media share links |
Authoring:
| Social-Share |
| ------------ |
| <a href="https://facebook.com/sharer/...">Facebook</a> <a href="http://twitter.com/share?...">Twitter</a> <a href="https://linkedin.com/shareArticle?...">LinkedIn</a> <a href="mailto:?...">Email</a> |
Features:
- Horizontal row of circular social media icon buttons
- Platform icons via inline SVG data URIs (Facebook, Twitter/X, LinkedIn, Email)
- Platform detected from link href
- Accessible:
aria-labelon each link,target="_blank"withrel="noopener noreferrer" - 40px circular buttons with grey border, hover darkens
Responsive behavior:
- All viewports: horizontal flex row, left-aligned
Location: /blocks/contact-card/
| Variant | Class | Purpose |
|---|---|---|
| Default | .contact-card |
Contact info card with title and two-column layout |
Authoring:
| Contact-Card |
| --- | --- |
| <h3>Title</h3> |
| <h4>Left Label</h4><p>Text</p><ul><li>links</li></ul> | <h4>Right Label</h4><p>Text</p> |
Features:
- White card (border-radius 5px, box-shadow
var(--shadow-card), max-width 1000px) - H3 title row (font-weight 500,
var(--heading-font-size-m)) - Two-column layout with H4 subheadings as eyebrow-style labels (uppercase, letter-spacing 2.08px,
var(--color-muted)) - Vertical separator between columns on desktop (
::afteron left column, 4px solidvar(--light-color)) - DA button reset: inline links in paragraphs and lists get
.buttonfromdecorateButtons(), CSS resets them - Contact list items: no bullets, standard link styling
Responsive behavior:
- Mobile: single column,
var(--spacing-l)gap between columns - Desktop (>=992px): side-by-side, left 58% + right flex-1, vertical separator
Location: /blocks/cards-leadership/
| Variant | Class | Purpose |
|---|---|---|
| Default | .cards-leadership |
Horizontal person cards with portrait image, yellow accent dash, name, and title |
Authoring:
| Cards-Leadership |
| --- | --- |
| <picture>portrait</picture> | <h3><a href="...">Person Name</a></h3><p>Title</p> |
| <picture>portrait</picture> | <h3>Person Name (no link)</h3><p>Title</p> |
Features:
- Horizontal card layout: portrait image left, text details right
- Yellow accent dash (32x3px) above name
- Entire card wraps in
<a>if h3 contains a link (clickable card) - Cards without links render without anchor wrapper
- Box shadow, 8px border-radius
- DA button reset for links inside cards
Responsive behavior:
- Mobile: single column, max-width 450px centered, 103px portrait
- Desktop (>=992px): 2-column grid (50% each), 180px portrait,
box-sizing: border-box
Used on: Leadership, UPS Foundation Leadership
Location: /blocks/cards-reports/
| Variant | Class | Purpose |
|---|---|---|
| Default | .cards-reports |
Horizontal document cards with thumbnail, title, and action link |
| cards-reports-text | .cards-reports.cards-reports-text |
Text-only document list without thumbnails |
Authoring (default):
| Cards-Reports |
| --- | --- |
| <picture>thumbnail</picture> | <h3>Document Title</h3><p><a href="...">Download</a></p> |
Authoring (cards-reports-text):
| Cards-Reports (cards-reports-text) |
| --- | --- |
| | <h3>Document Title</h3><p><a href="...">Download</a></p> |
Features:
- Horizontal card layout: document thumbnail left, details right
- H3 title + action link (Download, Learn More, View)
- Action link styled with bold, underline, link-color
- Box shadow, 8px border-radius
- DA button reset for action links
cards-reports-text specifics:
- Hides image column, full-width single-column list
- No card shadow, dotted bottom border between items
- Title in link color (blue), action link as pill button with border
- Action link has chevron
>after text - Horizontal flex layout: title left, action button right
Responsive behavior:
- Mobile: single column, max-width 450px centered, 103px thumbnail
- Desktop (>=992px): 2-column grid (50% each), 180px thumbnail,
box-sizing: border-box - Text variant: single column at all sizes
Used on: Reporting (default), Governance Documents, Code of Conduct, Political Engagement Archive (text variant)
Location: /blocks/awards-list/
| Variant | Class | Purpose |
|---|---|---|
| Default | .awards-list |
Year-tabbed list of award entries |
Authoring:
| Awards-List |
| --- | --- |
| 2026 | <p>Category</p><h3>Title</h3><p>Date · Description</p><p><a href="...">Read More</a></p> |
| 2026 | <p>Category</p><h3>Title</h3><p>Date · Description</p><p><a href="...">Read More</a></p> |
| 2025 | <p>Category</p><h3>Title</h3><p>Date · Description</p><p><a href="...">Read More</a></p> |
Features:
- Groups items by year (Col1), creates tab buttons per unique year
- Tab bar with teal active state (#0A8080), grey inactive (#5F5753)
- Active tab: 4px bottom border in teal
- Each item: eyebrow with yellow accent dash, h3 title, meta paragraph, Read More link with chevron
- Max-width 878px centered award list
- Separator lines between items
Responsive behavior:
- All viewports: single column list with sticky tab bar
- Tab bar scrollable on mobile if many years
Used on: Awards and Recognition
Location: /blocks/timeline/
| Variant | Class | Purpose |
|---|---|---|
| Default | .timeline |
Vertical timeline with period navigation and scroll spy |
Authoring:
| Timeline |
| --- | --- |
| 1907-1950 | <h3>Event Title</h3><p>Description</p> |
| 1907-1950 | <picture>period image</picture> |
| 1951-1975 | <h3>Event Title</h3><p>Description</p> |
Features:
- Groups items by period (Col1), renders period labels and events
- Events have year badge (yellow accent dash + year text), h3 title, description
- Images rendered full-width with 8px border-radius
- Desktop: sticky sidebar navigation with scroll spy (IntersectionObserver)
- Mobile: dropdown button with collapsible period menu
- Period labels centered on horizontal line with background pill
Responsive behavior:
- Mobile: full-width, dropdown period selector, events at full width
- Desktop (>=992px): 75% content + 25% sidebar, sidebar sticky, events indented 100px
Used on: Our History
Location: /blocks/form/
| Variant | Class | Purpose |
|---|---|---|
| Default | .form |
Styled form with various field types |
Authoring:
| Form |
| --- | --- |
| Field Label | text |
| Field Label | email |
| Field Label | textarea |
| Field Label | select: Option 1, Option 2, Option 3 |
| Submit | submit |
Features:
- Creates form fields from 2-column table rows (label + type)
- Supported types: text, email, textarea, select (with comma-separated options), submit
- Styled inputs with border, 8px radius, 14px padding
- Select with custom chevron SVG
- Submit button: gold CTA (#FFC400), pill border-radius
- Labels visually hidden (placeholder-only approach)
Responsive behavior:
- All viewports: max-width 600px centered form, full-width fields
Used on: Thank a UPS Hero
Location: /blocks/data-table/
| Variant | Class | Purpose |
|---|---|---|
| Default | .data-table |
Converts div block structure to native HTML <table> |
Authoring:
| Data-Table |
| --- | --- | --- |
| Header 1 | Header 2 | Header 3 |
| Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |
Features:
- JS converts the div-based block structure into a native
<table>element - First row becomes
<thead>with<th>cells, remaining rows become<tbody>with<td>cells - Styled with blue header row, alternating grey row backgrounds, centered non-first columns
- Necessary because the import pipeline (DOM → markdown → DA HTML) converts all
<table>elements to divs
Responsive behavior:
- All viewports: full-width table, horizontal scroll on narrow viewports via container
Used on: Gender Equality Index - UPS France
Location: /blocks/leadership-bio/
| Variant | Class | Purpose |
|---|---|---|
| Default | .leadership-bio |
Two-column bio layout: text left, portrait image right |
Authoring:
| Leadership-Bio |
| -------------- |
| <picture>portrait image</picture> |
| <h1>Name</h1><p>Job Title</p><p>Bio paragraph...</p><p><strong>Subheading</strong></p><p>More text...</p> |
Features:
- 2-row block: Row 1 = portrait image, Row 2 = text content (name, title, bio paragraphs)
- JS restructures into two-column layout with
.leadership-bio-container - H1 name: 48px desktop / 40px mobile, font-weight 500
- Job title: first
<p>styled as muted grey (var(--color-muted)), 24px desktop / 16px mobile - Body paragraphs: 16px, 32px margin-bottom
- Bold subheadings (
<strong>) rendered at normal weight (CSS overridefont-weight: inherit) - Image set to eager loading (JS overrides
loading="lazy") - DA button reset styles included
Responsive behavior:
- Mobile: column-reverse (image on top, text below), full-width image, no border-radius
- Desktop (>=992px): row layout, 58.333% text + 33.333% image with auto margin-left, 8px border-radius, 80px margin-top on image to align with body text
Used on: All 20 company leadership bios, 1 foundation leadership bio (nikki-clifton)
Location: /blocks/investor-links/
| Variant | Class | Purpose |
|---|---|---|
| Default | .investor-links |
Centered quick links with icons (Email Alerts, Contacts) |
Authoring:
| Investor-Links |
| -------------- |
| [Email Alerts](url) |
| [Contacts](url) |
Features:
- Horizontal centered layout with icons prepended to each link
- Icon detection from link text: "email"/"alert" → envelope icon (✉), "contact" → person icon (👤)
- Icons via Unicode characters in
::beforepseudo-elements on.investor-links-iconspans - Border-top separator (
1px solid var(--color-border)) - DA button reset: removes all button styling from links
Responsive behavior:
- All viewports: horizontal flex row, centered,
var(--spacing-l)gap
Used on: All 6 governance sub-pages (governance-documents, board-committees, contact-the-board, ups-code-of-conduct-and-ethics, political-engagement-policy, political-engagement-policy/archive)
Import scripts for bulk content migration are in /tools/importer/.
import-universal.bundle.js for all page imports. The universal script supersedes all per-template import scripts. It handles every page type on the UPS site automatically:
- Article pages: Auto-detected via
.pr15-detailsselector → article-header, body content, embed, social-share, related stories - Standard pages: All other pages → block registry detection, DOM walking, section grouping with wrapper-aware styles
The script includes all 23 block parsers and the cleanup transformer. It detects section wrapper contexts (arc, highlight, arc-wave) before cleanup runs, then applies appropriate section-metadata styles in the output.
Bundling (must re-bundle after ANY change to import-universal.js, parsers, or transformers):
npx esbuild tools/importer/import-universal.js --bundle --format=iife --global-name=CustomImportScript --outfile=tools/importer/import-universal.bundle.js--format=iife --global-name=CustomImportScript. The bulk import runner injects the script as a <script> tag and looks for window.CustomImportScript.default. ESM format (--format=esm) will NOT work.
Usage:
node run-bulk-import.js --import-script tools/importer/import-universal.bundle.js --urls urls-file.txt| File | Purpose |
|---|---|
page-templates.json |
Template definitions mapping source URL patterns to blocks |
import-universal.js |
Universal import script — use this for ALL pages |
import-universal.bundle.js |
Bundled universal script (passed to run-bulk-import.js) |
parsers/cards-awards.js |
Parser for cards-awards block |
parsers/cards-stories.js |
Parser for cards-stories block |
parsers/columns-feature.js |
Parser for columns-feature block |
parsers/columns-quote.js |
Parser for columns-quote block |
parsers/columns-stats.js |
Parser for columns-stats block (home page) |
parsers/fact-sheets.js |
Parser for fact-sheets block (our-company page) |
parsers/hero-featured.js |
Parser for hero-featured block |
parsers/columns-media.js |
Parser for columns-media block — handles hero grid (.herogrid) and list container (#list-container) patterns |
parsers/article-header.js |
Parser for article-header block (story article pages) |
parsers/embed.js |
Parser for embed block (YouTube iframe → watch URL link) |
parsers/social-share.js |
Parser for social-share block (social media share links) |
parsers/contact-card.js |
Parser for contact-card block (Media Relations section on newsroom) |
parsers/navigation-tabs.js |
Parser for navigation-tabs block |
parsers/cards-leadership.js |
Parser for cards-leadership block (leadership portraits) |
parsers/cards-reports.js |
Parser for cards-reports block (report document cards) |
parsers/timeline.js |
Parser for timeline block (our-history page) |
parsers/awards-list.js |
Parser for awards-list block (awards-and-recognition page) |
parsers/form.js |
Parser for form block (contact/speaker request forms) |
parsers/leadership-bio.js |
Parser for leadership-bio block (executive/foundation bios) |
parsers/governance-cards.js |
Parser for cards-reports on investors.ups.com governance page |
parsers/governance-subnav.js |
Parser for navigation-tabs on investors.ups.com governance page |
parsers/governance-banner.js |
Parser for page banner on investors.ups.com governance pages |
parsers/governance-asset-list.js |
Parser for document download lists (.module-asset-list) → cards-reports-text block |
parsers/governance-table.js |
Parser for board committee HTML table → data-table block |
parsers/investor-links.js |
Parser for investor quick links (Email Alerts, Contacts) on governance pages |
parsers/footer-funnel.js |
Parser for footer funnel links (navigation-tabs) — currently blocked by cleanup transformer |
transformers/ups-cleanup.js |
Site-wide DOM cleanup transformer (includes investor site footer link removal) |
Icons (/icons/):
search.svg— Search iconups-logo.svg— UPS logo
Fonts (/fonts/):
roboto-regular.woff2,roboto-medium.woff2,roboto-bold.woff2— Roboto web fontsroboto-condensed-bold.woff2— Roboto Condensed Boldupspricons.woff— UPS icon font (button chevron\e60f)
Always use CSS Color Level 4 syntax:
/* ✓ Correct */
color: rgb(0 0 0 / 95%);
background: rgb(255 255 255 / 50%);
/* ✗ Avoid */
color: rgba(0, 0, 0, 0.95);
background: rgba(255, 255, 255, 0.5);- Always use tokens for: colors, spacing, typography, shadows, transitions
- Define new tokens only if a value is used 2+ times across different files
- Keep hardcoded intentional design dimensions (specific widths, icon sizes)
Use consistent section headers:
/* ===== SECTION NAME ===== */- Scope all styles to the block class:
.my-block .child-element - Avoid external context selectors unless necessary (e.g.,
.section.highlight .my-block) - Use
:has()on wrapper for edge-to-edge blocks:main > div:has(.my-block)
When setting explicit width/height on elements that also have padding:
/* ✓ Correct - dimensions include padding */
.card {
box-sizing: border-box;
width: 160px;
height: 120px;
padding: 16px;
}
/* ✗ Wrong - actual size will be 192x152px */
.card {
width: 160px;
height: 120px;
padding: 16px;
}Always use the variable, never hardcode:
/* ✓ Correct */
font-family: var(--body-font-family);
/* ✗ Avoid */
font-family: 'Helvetica Neue', sans-serif;Export only the default decorate function:
// ✓ Correct
export default function decorate(block) { ... }
// ✗ Avoid - unless function is imported elsewhere
export function showSlide() { ... }- Use
document.createElement()for structural elements innerHTML = ''is acceptable for clearing containersinnerHTMLwith template literals is acceptable for:- Fully controlled static content (no user input)
- Simple markup that would be verbose with createElement
- Always scope queries to
block:block.querySelector('.child')
Always include ARIA attributes on interactive elements:
aria-labelon buttons without visible textaria-hiddenon decorative elementsaria-expandedon toggleable sections
- Screenshots →
/tmp/ONLY - Never save to project root or workspace - Always read files before editing
- Test in preview at localhost:3000
- Check hover states - many elements have specific behaviors
- Follow existing patterns in the codebase
- Update this file when learning new project-specific patterns
- Use
box-sizing: border-boxwhen setting width/height on padded elements - Fragment files (nav.html, footer.html) must NOT have
<header>or<footer>tags - Merge similar blocks into single multi-row blocks - don't create separate blocks for each row of similar content
- Page-specific styles stay page-specific - When importing styles from one page to match another, NEVER modify shared block CSS in ways that affect other pages
- CSS variable naming - NEVER use
--spacing-sm,--spacing-md,--spacing-lg. The correct names are--spacing-s,--spacing-m,--spacing-l. Using incorrect names will silently fail. - Links vs Buttons - In EDS, links that are alone in a paragraph (
<p><a>...</a></p>) become buttons styled by global styles. If a block needs specific button styling, the block CSS must override the global button styles using block-scoped selectors. - Default content centering is global - Centering of
.default-content-wrappercontent applies to ALL pages unconditionally. - Template meta tag in HTML head - The
decorateTemplateAndTheme()function reads<meta name="template" content="...">from the<head>, NOT from the metadata block in the body. When creating new page HTML files, always add<meta name="template" content="template-name"/>to the<head>if the page uses a template. - CSS variables: always verify before using - Before using ANY CSS variable in your code, verify it exists in
styles.css. CSS variables that don't exist silently resolve to nothing. - Block CSS must not override global button styles with link styles - In EDS,
a.buttongets global button styling. Block CSS should NEVER setcolor: var(--link-color)ona.buttonelements. - Absolute-position
<picture>for background images - When using a<picture>element as a background (inside an absolutely-positioned container), the<picture>must also beposition: absolute; inset: 0. Settingheight: 100%alone on<picture>does not reliably stretch it to fill the parent. - Lazy loading breaks after DOM restructuring - When a block JS moves images from original DOM positions to new containers, set
img.loading = 'eager'on allimg[loading="lazy"]elements in the block. - Don't use
createOptimizedPicturefor external images - During migration, images reference external URLs.createOptimizedPicturestrips the domain and creates broken local paths. Leave external images as-is. - All-caps content → CSS text-transform - Never import all-caps text literally. Convert to Title Case in content and apply
text-transform: uppercasevia CSS on the target element. - Block-wide bold → CSS font-weight - Don't wrap entire block elements in
<strong>. Applyfont-weight: 700via CSS targeting the element's position (e.g.,p:first-child). Reserve<strong>for inline emphasis only. - Margin-driven spacing system - Sections have
padding: 0by default so wrapper margins collapse through them for cross-section gaps. Block wrappers = 80px margin-top, default-content = 40px base (overridden to 32px/24px for same-section siblings). Background sections (highlight) usepadding: 80px 0with first/last child margin reset to 0. Never add section padding to regular sections. - Never push to Git yourself - The user handles all Git operations (commit, push, branch). Only modify files — leave Git workflow to the user.
- Content and code are strictly separated - Content (HTML) lives in DA (CMS), code (JS/CSS) lives in Git. Never commit HTML content to Git. Never modify
.gitignoreto track HTML files. - DA wraps inline content in
<p>tags - Block JS/CSS must use flexible selectors (e.g.,:scope > a, :scope > p > a) to handle both direct children and p-wrapped children from DA. Never add JS unwrapping logic — fix compatibility in CSS with button resets and in JS with dual selectors. - Fragment default paths are root-relative -
header.jsdefaults to/nav,footer.jsdefaults to/footer. Local dev pages override these via<meta name="nav" content="/content/nav"/>. On deployed (DA), no override exists — the default root path is used. p.button-wrappermust havemargin: 0— The globalp.button-wrapperrule must NOT add margin. Spacing is handled by the* + *rule inside default-content-wrapper. Extra margin on button-wrapper leaks through section boundaries (where sections havepadding: 0) and creates incorrect gaps.- Content files use
.plain.htmldiv format — Blocks as<div class="block-name">, sections as top-level<div>wrappers, metadata as<div class="metadata">. No page shell, no<hr>separators. See ".plain.htmlContent Format" in Migration Rules. - Import scripts: use DOM-walking, not rigid section assembly — Parsers call
element.replaceWith(blockDiv)which detaches the original element reference. Never search staleblock.elementafter parsing. Instead, walk the DOM after all parsers run to collect block divs and default content in natural document order. This also handles pages with different block orders using the same template. Seeimport-universal.jsfor the reference implementation. .plain.htmlis the single source of truth — All content edits go directly in.plain.htmlfiles. No.htmlor.mdfiles should exist in the content folder. This is the format DA consumes and produces.- Always use
import-universal.bundle.jsfor all imports — The universal import script handles every page type (articles and standard pages). No per-template scripts exist — only the universal script. - Data tables in
.plain.html— The.plain.htmlformat converts all tables to divs. Use thedata-tableblock for data tables: the import script outputsData-Tableas the block name, anddata-table.jsconverts the div structure back to a native<table>at decoration time. - Keep sitemap blocks[] current after every content change — After imports, re-imports, or any content modification, update the affected page's
blocks[]andsectionStyles[]in/sitemap.json. Before refactoring block CSS/JS, query the sitemap to find all affected pages. - NEVER allow
.html(non-.plain.html) or.mdfiles in/content/— The content area must contain ONLY.plain.htmlfiles. Delete any.htmlor.mdfiles on sight. Import scripts must never produce these formats. - Parser-first, always — Never edit
.plain.htmldirectly as a first approach. Update parsers → re-bundle → re-import. Direct edits are a last resort and MUST be followed by updating all impacted parsers and running a test import to verify alignment. - Check sitemap BEFORE modifying any parser — Query
/sitemap.jsonto find all pages using the affected block. If validated/approved pages exist, warn the user about potential regressions before proceeding with the parser change. - Assess re-import needs after every block/parser/style change — Structural parser changes require re-importing ALL affected pages. CSS-only changes may only need re-validation. Always flag affected pages from the sitemap to the user and let them decide the scope.
- Update sitemap after EVERY import without exception — Updating
blocks[]andsectionStyles[]in the sitemap is a mandatory part of every import operation, not a follow-up task. The import is not complete until the sitemap reflects the new content. - Direct
.plain.htmledits create parser debt — If you must edit content directly, immediately update impacted parsers AND test via re-import. Unresolved parser/content drift guarantees regressions on the next bulk import.
@media (width < 992px) {
.image-container {
max-width: 500px;
margin: 0 auto;
}
}/* Container: position relative */
.block .inner {
position: relative;
overflow: hidden;
}
/* Picture: absolute-positioned to fill container */
.block .inner > picture {
position: absolute;
inset: 0;
z-index: 0;
display: block;
}
/* Image: fill and cover */
.block .inner > picture img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* Content: positioned above image */
.block .content {
position: relative;
z-index: 1;
}main .default-content-wrapper blockquote {
border-left: 4px solid var(--color-gold);
padding-left: var(--spacing-m);
margin: 0;
font-style: italic;
text-align: left;
}
main .default-content-wrapper blockquote p {
margin: 0;
}When DA wraps links in <p> tags, decorateButtons() applies .button and .button-wrapper classes. Reset them in block CSS:
/* Reset button styling from decorateButtons() on DA-wrapped links */
.block-name a.button:any-link {
display: inline;
margin: 0;
border: none;
border-radius: 0;
padding: 0;
background: none;
color: currentcolor;
font-size: inherit;
font-weight: inherit;
white-space: nowrap;
}
.block-name a.button:any-link:hover {
background: none;
border: none;
}
.block-name a.button:any-link::after,
.block-name .button-wrapper {
all: unset;
}/* Section: creates stacking context */
main > .section.arc {
position: relative;
z-index: 0;
}
/* ::after: SVG positioned at bottom behind content */
main > .section.arc::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: auto;
padding-top: calc(5%); /* responsive height based on section width */
background-image: url("data:image/svg+xml,...");
background-repeat: no-repeat;
background-size: 100%;
background-color: transparent;
z-index: -1;
pointer-events: none;
}main .default-content-wrapper table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: var(--body-font-size-s);
}
main .default-content-wrapper table th {
background: var(--link-color);
color: #fff;
font-weight: 700;
padding: var(--spacing-xs) var(--spacing-s);
text-align: center;
}
main .default-content-wrapper table td {
padding: var(--spacing-xs) var(--spacing-s);
border-bottom: 1px solid var(--color-border);
}
main .default-content-wrapper table td:not(:first-child) {
text-align: center;
}Source URL: https://about.ups.com/us/en/home.html
Site structure (confirmed from migration):
- Header: UPS logo (60px), horizontal navigation (Our Stories, Our Company, Our Impact, Investors, Newsroom), utility links (ups.com, Support). Mega menu dropdowns on desktop with full-width panels.
- Hero: Full-width h1 heading "Moving our world forward by delivering what matters" with yellow accent bar below, centered text.
- Featured content (hero-featured): Background image with white card overlay — eyebrow, h4 heading, description, gold CTA.
- Story cards (cards-stories): 3-column grid of clickable story cards with image, eyebrow, title, description.
- About section: Centered text with h6 eyebrow ("About Us"), h2 heading, CTA button. Uses
accent-barsection style. - Stats (columns-stats): Full-width image with overlapping white stats panel — ~460K Employees, 200+ Countries, 20.8M Packages/day, $88.7B Revenue, gold CTA.
- Impact section (columns-feature): Two-column with image left, text right — eyebrow, h2 heading, description, CTA.
- Footer: Highlighted links strip (Newsroom, Careers), 4-column links grid (This Site, Other UPS Sites, Connect, Subscribe), legal links row, copyright.
Brand colors (confirmed):
- Gold/Yellow CTA buttons:
#ffc400(background),#e0ac00(hover) - Yellow accent elements:
#ffd100(eyebrow dash),#ffdc40(heading bar) - Text:
#242424(primary),#505050(secondary) - Links:
#426da9(default),#244674(hover) - Backgrounds:
#fff(white),#f2f2f2(light grey/highlight)
Typography: Roboto (regular 400, medium 500, bold 700), Roboto Condensed Bold. Font weights: headings use 500, body 400, eyebrows/buttons 700.