Operator: smaller SERP thumbnails + a right column (featured products, fallback random; all SERP + shop home). - Thumbnails: serp-list thumb column 140/200/260/320 -> 88/110/130/150 across container tiers (frees room for body + the new rail). - _featured_rail_products(request, shop) in shop.py: shop's curated featured_product_ids (public/ready/deduped/capped); random public-ready fallback so it's never empty; bounded func.random() query (CWE-407-safe). Injected into _build_home_layout_context (home/shop/search) + shop_tag_detail ctx. - Shared _facet_nav.j2:featured_rail macro, rendered as the 3rd child of the existing .tag-detail-layout on shop_tag.j2 / home.j2 / shop.j2 (within show_facet_nav). - CSS: <1100px rail is full-width beneath results; >=1100px 3-col facet | results | sticky rail; <800px stacks. Grid-only, tokens. /styleguide#serprail added. - Tests: +test_serp_featured_rail_renders_with_random_fallback, +test_serp_featured_rail_prefers_curated_featured; test_tag_detail_price_filter_narrows_grid rescoped to the results grid (rail is unfiltered discovery by design). 1139 passed. Docs: mps-24.md Phase 2.8l, design-system.md.
19 KiB
MPS Design System
Overview
Make Post Sell uses a design token architecture with CSS custom properties as the single source of truth. Mobile-first. CSS Grid only (no Flexbox). Accessible. Respects reduced motion. Supports light and dark themes.
Architecture Diagram
┌─────────────────────────────────────────────────────────┐
│ tokens.css │
│ Design Tokens (:root) │
│ │
│ Colors · Typography · Spacing · Shape · Elevation │
│ Motion · Z-index · Layout · State overlays │
│ │
│ [data-theme="dark"] overrides │
│ │
│ Base resets · Focus · Selection │
│ Typography utilities (.type-*) │
│ Elevation utilities (.elevation-*) │
│ Surface utilities (.surface-*) │
│ State layer · Ripple effect │
│ Animations · Skeleton loading · Spinner │
│ Spacing utilities (.mt-*, .mb-*, .p-*, .gap-*) │
│ Container utilities (.container, -narrow, -wide) │
└──────────────────────┬──────────────────────────────────┘
│ consumed by
▼
┌─────────────────────────────────────────────────────────┐
│ common.css │
│ Component Styles │
│ │
│ Body layout · Navigation · Forms · Buttons │
│ Product grid · Cards · Cart · Comments │
│ Wells · Alerts · Ribbons · Footer │
│ Watch mode · Landing page · Login │
│ Responsive breakpoints │
│ │
│ References tokens via var(--token-name, fallback) │
└──────────────────────┬──────────────────────────────────┘
│ rendered in
▼
┌─────────────────────────────────────────────────────────┐
│ templates/ │
│ Jinja2 Templates │
│ │
│ base.j2 → theme toggle, nav, footer │
│ styleguide.j2 → live component reference │
│ product.j2, content.j2, shop.j2, etc. │
└─────────────────────────────────────────────────────────┘
File Map
| File | Lines | Purpose |
|---|---|---|
static/css/tokens.css |
775 | Design tokens, utilities, animations, resets |
static/css/common.css |
3879 | Component styles consuming tokens |
templates/styleguide.j2 |
1083 | Live styleguide at /styleguide |
views/misc.py:23 |
— | Styleguide view controller |
Token Categories
Colors
Brand Surface Text Border
────────── ────────── ────────── ──────────
--color-green --surface-base --text-primary --border-default
--color-blue --surface-dim --text-body --border-light
--color-navy --surface-container --text-secondary --border-focus
--color-purple --surface-container- --text-muted --border-error
--color-danger high --text-faint
--color-gold --surface-inverse --text-disabled
--text-inverse
Typography Scale (Major Third 1.250)
Token Size Use
────────── ────── ──────────────────────
--text-xs 12px Captions, overlines
--text-sm 14px Labels, small body
--text-base 16px Body text (root)
--text-md 18px Large body
--text-lg 20px Titles
--text-xl 24px Title large
--text-2xl 30px Headline 3
--text-3xl 36px Headline 2
--text-4xl 48px Headline 1
--text-5xl 60px Display
Spacing Scale (4px base)
Token Value
────────── ──────
--space-0 0
--space-1 4px
--space-2 8px
--space-3 12px
--space-4 16px
--space-5 20px
--space-6 24px
--space-8 32px
--space-10 40px
--space-12 48px
--space-16 64px
--space-20 80px
--space-24 96px
Shape (Border Radius)
--radius-none 0 Sharp corners
--radius-sm 4px Inputs, code blocks
--radius-md 8px Cards, buttons
--radius-lg 12px Modals, panels
--radius-xl 16px Large surfaces
--radius-2xl 24px Pills
--radius-full 9999px Circles
Elevation (Box Shadow)
--elevation-0 none Flat
--elevation-1 subtle Cards at rest
--elevation-2 low Raised cards
--elevation-3 medium Dropdowns
--elevation-4 high Modals
--elevation-5 highest Popovers
Motion
Durations Easing
────────────────── ──────────────────
--duration-instant 50ms --ease-standard general transitions
--duration-fast 100ms --ease-decelerate entrances
--duration-normal 200ms --ease-accelerate exits
--duration-slow 300ms --ease-emphasize emphasis
--duration-slower 400ms --ease-spring playful bounce
--duration-entrance 250ms
--duration-exit 200ms
Z-Index Scale
--z-base 0 Default stacking
--z-dropdown 100 Dropdowns, popovers
--z-sticky 200 Sticky headers
--z-overlay 300 Overlays, backdrops
--z-modal 400 Modals
--z-toast 500 Toast notifications
--z-ribbon 600 Shop ribbon banner
Layout Breakpoints
--content-narrow 400px Login forms, narrow content
--content-width 800px Default content width
--content-wide 1200px Wide layouts
--bp-tablet 800px Tablet breakpoint
--bp-desktop 1200px Desktop breakpoint
Theme System
Light mode is default (:root). Dark mode activates via [data-theme="dark"] on the <html> element. The dark theme overrides all semantic tokens — surfaces, text, borders, brand colors — so components adapt automatically without per-component dark mode rules.
Light Dark
────────────────────── ──────────────────────
--surface-base: #FFFFFF --surface-base: #0d1117
--text-primary: #333333 --text-primary: #ffffff
--border-default: #e0e0e0 --border-default: #7ab9ff
--color-green: #a3c765 --color-green: #08e700
--color-navy: #5871ad --color-navy: #7ab9ff
Theme toggle: window.setTheme('dark') / window.setTheme('light').
Typography Utilities
CSS classes that compose token values into complete type styles:
| Class | Size | Weight | Use |
|---|---|---|---|
.type-display |
clamp(36px, 5vw, 60px) | bold | Hero headlines |
.type-headline-1 |
48px | bold | Page titles |
.type-headline-2 |
36px | bold | Section titles |
.type-headline-3 |
30px | bold | Subsection titles |
.type-title-lg |
24px | semibold | Card titles |
.type-title |
20px | semibold | List titles |
.type-title-sm |
16px | semibold | Small titles |
.type-body-lg |
18px | regular | Lead paragraphs |
.type-body |
16px | regular | Body text |
.type-body-sm |
14px | regular | Secondary text |
.type-label-lg |
14px | semibold | Form labels |
.type-label |
12px | semibold, uppercase | Overline labels |
.type-caption |
12px | regular | Captions |
.type-overline |
11px | bold, uppercase | Section overlines |
.type-code |
0.9em | mono | Inline code |
Component Library
All components are documented with live examples at /styleguide. The styleguide is the single source of truth for the component library. If it is not in the styleguide, it does not exist as a pattern.
Styleguide Sections
| Section | ID | Description |
|---|---|---|
| Tokens | #tokens |
Raw token reference table |
| Colors | #colors |
Brand, surface, text, border, alert swatches |
| Typography | #typography |
Type scale and utility classes |
| Elevation | #elevation |
Shadow levels |
| Motion | #motion |
Animations, transitions, easing |
| Spacing | #spacing |
Spacing scale visualization |
| Shape | #shape |
Border radius samples |
| States | #states |
Interactive state layers |
| Loading | #loading |
Skeleton and spinner patterns |
| Buttons | #buttons |
Button variants (green, blue, red, navy, outline) |
| Forms | #forms |
Input fields, textareas, selects, settings-form (.settings-form / .settings-form-grid / .settings-field / .settings-field-hint) |
| Wells | #wells |
Content wells and containers |
| Alerts | #alerts |
Success, info, warning, danger alerts |
| Status | #status |
Status indicators |
| Product Cards | #cards |
Product grid cards, profile card (.profile-card-header / .profile-avatar / .profile-handle / .profile-email-reveal), action button grid (.action-columns / .action-button-grid), MPS-24 tag chips (#tag-chips), MPS-24 sectioned lanes (#tag-lanes) |
| Cart | #cart |
Cart and checkout components |
| Gift Cards | #gift-cards |
Gift card purchase, balance check, management |
| Comments | #comments |
Comment form and list |
| Toggle | #toggle |
Toggle switches |
| Ribbon | #ribbon |
Shop ribbon banner |
| Environment Banner | .environment-banner |
Staging/dev environment indicator |
| Trial Banner | .trial-banner |
Trial countdown and expiry notice |
| Task Bar | #taskbar |
Task bar component |
| Layout | #layout |
Grid layout patterns |
| Theme System | #theme |
Theme toggle and dark mode |
| Footer | #footer |
Footer component |
MPS-20 / MPS-21 components
| Class | Where | Description |
|---|---|---|
.cart-negotiation-card |
cart.j2 |
Soft-green "Offer accepted / Auction won" well at the top of a negotiated cart |
.cart-negotiation-deadline |
cart.j2 |
Pay-by countdown line inside the negotiation card |
.cart-negotiation-pill |
cart.j2 |
"Offer accepted · quantity locked" pill on line items |
.offer-pay-cta-actions |
offer.j2 |
Two-col grid: Cancel left, Pay right (stacks on narrow viewports) |
.offer-pay-deadline-note |
offer.j2 |
Buyer's "Payment due in 23 hours, 14 minutes" copy |
.offer-pay-link-row |
offer.j2 |
Seller's shareable offer-link with Copy button |
.offer-respond-deadline-note |
offer.j2 |
"Respond in 5 days, or this offer auto-expires" |
.auction-winner-pay |
auction.j2 |
"You won this auction!" well with countdown + pay CTA |
.product-add-disabled-note |
product.j2 |
Caption under disabled Add To Cart when active cart is locked |
.shop-offers-page |
user_offers.j2 + user_bids.j2 + shop_offers.j2 |
Wider page (max-width 1100px) overriding .one-column for the offers/bids inbox tables |
.shop-offers-table |
inbox templates | Responsive table; collapses to per-row block list below 720px |
.notification-badge |
base.j2 + /u/settings |
Danger-color pill showing unread notification count |
.notification-row + .notification-row-unread |
user_notifications.j2 |
Notification list rows; read rows fade to 0.65 opacity, unread rows get an alert-info background + navy left border |
.notification-row-breadcrumbs |
user_notifications.j2 |
Shop › Product › Offer/Auction/Invoice chain under each row |
.billing-page / .billing-paypal-card / .billing-actions |
billing.j2 |
/billing redesigned with Grid + tokens (no inline styles) |
[data-pay-deadline] |
offer + auction + cart | Convention attribute the shared static/js/pay-countdown.js (and inline tickers in offer.js / auction.js) rewrite once per second with a prose human delta ("in 23 hours, 14 minutes, 8 seconds") matching the server-rendered ago.human() fallback |
MPS-24 components — tags, chips, lanes
| Class | Where | Description |
|---|---|---|
.tag-chip-strip |
home.j2, shop.j2, shop_tag.j2 |
Horizontally scrolling row of chips at the top of the shop home; grid-auto-flow: column + overflow-x: auto so chips swipe on mobile |
.tag-chip |
every chip strip + bulk tagger | Pill-shaped link, tokens-only colors, hover brightens border + bg |
.tag-chip-active |
current filter chip | Accent-fill chip; click on home filters the grid in place via static/js/tag_filter.js |
.tag-lane |
home.j2, shop.j2 (layout 2) |
One <section> per top tag; capped product count via shop.home_layout_per_lane_limit |
.tag-lane-header |
inside .tag-lane |
Grid 1fr / auto — lane title left, "See all →" right |
.tag-lane-grid |
inside .tag-lane |
Reuses .serp grid pattern; horizontal lane is the outer layout, items inside still use product grid |
.tag-list / .tag-list-item |
shop_tags.j2 (bulk tagger) |
Operator tag overview: tag pill + product count + view / delete row |
.tag-product-list / .tag-product-row |
shop_tags.j2 (bulk tagger) |
"Apply / Applied" toggle per product per tag |
.tag-detail-header |
shop_tag.j2 |
Tag detail page header (tag name + back link) |
[data-tag-strip] |
chip strip | JS hook for tag_filter.js |
[data-tag-grid] |
flat .serp |
JS hook — items inside carry data-tag-slugs for in-place filter |
[data-tag-slugs] |
.serp-item |
Space-separated tag slugs the item carries; consumed by tag_filter.js |
.tag-suggest-list / .tag-suggest-item |
shop_tags.j2 (Phase 2) |
Suggested-cluster well rendered when operator clicks "Suggest categories from titles + descriptions"; per-cluster grid with label, sample titles, Apply / Dismiss actions |
.tag-suggest-actions |
shop_tags.j2 |
Apply / Dismiss button row, grid-auto-flow: column |
.product-tag-chips / .tag-chip-list |
product_edit.j2 (Phase 2.7) |
Per-product SPA tag editor container; js-only, revealed by product_tags.js. Chips wrap as inline-level pills (the <ul> is deliberately not a grid/flex parent) |
.tag-chip-removable |
product_edit.j2, /styleguide#tagchips |
Same pill language as .tag-chip but an <li> with a label + an always-visible .tag-chip-removable-x remove button (no hover-only controls). inline-grid auto auto |
.tag-chip-add |
product_edit.j2 |
Add-a-tag row, grid 1fr auto, stacks to one column under 600px |
.tag-chip-flash |
product_edit.j2 |
Toast region reusing .tag-flash-toast / .tag-flash-{success,error,info} |
[data-product-tags] / [data-product-tags-url] |
product_edit.j2 |
JS hooks for product_tags.js: container + the /p/{id}/tags endpoint URL |
.serp-rail / .serp-rail-list / .serp-rail-card |
_facet_nav.j2:featured_rail macro on shop_tag.j2 / home.j2 / shop.j2 (Phase 2.8l) |
Right rail on SERP pages — curated featured products or random fallback. 3rd grid child of .tag-detail-layout; full-width below results 800–1099px, sticky right column ≥1100px. /styleguide#serprail |
[data-focus-section] / [data-focus-heading] / [data-focus-list] |
shop_tags.j2 (Phase 2.8) |
Stable bulk-tagger focus container — always in the DOM, hidden until a tag is focused; tag_bulk.js swaps the product list in place instead of a full reload |
[data-tag-focus-link] |
shop_tags.j2 (Phase 2.8) |
Tag chip in the All-tags list; tag_bulk.js intercepts the click and fetches ?focus=<slug> as JSON |
.tag-list-dragging / .tag-list-drop-target |
shop_tags.j2 (Phase 2.8) |
Dragged row (dimmed) + active drop position during HTML5 drag-to-reorder; tag_bulk.js POSTs set_order on drop |
CSS Conventions
Layout
- CSS Grid only — never use Flexbox for layout
- Mobile-first: base styles target mobile,
@mediaqueries enhance for larger screens - Primary breakpoint:
max-width: 800pxfor mobile
Token Consumption
Components in common.css reference tokens with fallbacks:
/* Good — token with fallback for resilience */
background-color: var(--surface-base, #ffffff);
border-radius: var(--radius-sm, 4px);
/* Good — token without fallback (tokens.css always loaded) */
padding: var(--space-4);
Media Sizing
Never combine width: 100% with max-height on media elements. Use:
/* Correct */
width: auto;
max-width: 100%;
max-height: 33vh;
/* Wrong — creates dead whitespace */
width: 100%;
max-height: 33vh;
Mobile Usability
Never use hover-only interactions. All interactive elements must be always visible and tappable. Design touch-first, then optionally enhance for desktop hover.
Reduced Motion
All animations respect prefers-reduced-motion: reduce via a global media query in tokens.css that collapses durations to near-zero.
Capability-Driven Presentation
Follow Russell Ballestrini's capability-driven presentation practice. A page need not look identical across all browsers. Accommodate what the user's browser can do. Use the js-only / <noscript> pattern for progressive enhancement.
Adding New Components
- Define the component styles in
common.css, consuming tokens fromtokens.css - Add a live example to
/styleguide(templates/styleguide.j2) - If the component participates in watch mode SPA navigation, update all three layers: template,
watch.js,watch.py
Load Order
base.j2
└─ <link> static/css/tokens.css ← tokens + utilities + resets
└─ <link> static/css/common.css ← components consuming tokens
└─ per-page <style> blocks ← page-specific overrides