From 815876daf1c9a3de7358870a07afffdb1810c747 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 6 Mar 2026 09:18:12 -0500 Subject: [PATCH 01/31] feat: integrate design tokens from www.makepostsell.com styleguide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tokens.css as foundation layer (loaded before common.css) - Color system: brand, surface, text, border, state, alert tokens - Typography: major third scale with 14 semantic classes - Spacing: 4px base, 12-step scale with utility classes - Shape: 7 radius levels from none to pill - Elevation: 5 shadow levels with dual-shadow technique - Motion: 4 easing curves, 7 durations, entrance animations - Loading: skeleton shimmer + spinner (3 sizes) - Scroll reveal, state layers, ripple effect, focus rings - Accessibility: prefers-reduced-motion, :focus-visible - Rewrite styleguide.j2 to comprehensive design system reference - New sections: tokens, elevation, motion, spacing, shape, states, loading, status notices, cart buttons, checkout layout, toggle - All app components documented with live demos - TOC navigation for all sections - Add STYLEGUIDE rule to CLAUDE.md - Wire tokens.css into base.j2 before common.css Exploration branch — tokens.css provides new variables alongside existing common.css variables. No visual regressions expected as common.css values take precedence for shared property names. --- CLAUDE.md | 2 + make_post_sell/static/css/tokens.css | 651 ++++++++++++++++++++ make_post_sell/templates/base.j2 | 3 +- make_post_sell/templates/styleguide.j2 | 821 +++++++++++++++++++++---- 4 files changed, 1346 insertions(+), 131 deletions(-) create mode 100644 make_post_sell/static/css/tokens.css diff --git a/CLAUDE.md b/CLAUDE.md index 5b4ed1a..0ff5a60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -232,6 +232,8 @@ Always use `uuid_str` when you need a string copy of the identifier. Models inhe **CSS LAYOUT REQUIREMENTS**: This project uses CSS Grid exclusively for layout. NEVER use Flexbox (flex) for layout. Always use CSS Grid properties for positioning and alignment. +**STYLEGUIDE**: When creating new UI components (buttons, wells, alerts, layout patterns, etc.), add a live example to `/styleguide` (`make_post_sell/templates/styleguide.j2`). The styleguide is the single source of truth for the component library. If it's not in the styleguide, it doesn't exist as a pattern. + **CSS MEDIA SIZING**: Never combine `width: 100%` with `max-height` on media elements (img, video). `width: 100%` forces the element to span the full container even when `max-height` constrains the rendered content, creating dead whitespace. Use `width: auto` + `max-width: 100%` + `max-height` instead — the element shrinks to match the actual content aspect ratio within both constraints. **MOBILE USABILITY**: Never use hover-only interactions (`:hover` to reveal controls, `opacity: 0` with hover reveal, etc.). Mobile/touch devices have no hover state — controls hidden behind hover are invisible and unreachable. All interactive elements (buttons, toggles, links) must be always visible and tappable. Design touch-first, then optionally enhance for desktop hover. diff --git a/make_post_sell/static/css/tokens.css b/make_post_sell/static/css/tokens.css new file mode 100644 index 0000000..2ae157a --- /dev/null +++ b/make_post_sell/static/css/tokens.css @@ -0,0 +1,651 @@ +/* ============================================ + DESIGN TOKENS — Make Post Sell + Single source of truth for the design system. + CSS custom properties, consumed everywhere. + ============================================ */ + +:root { + + /* ---- Color: Brand ---- */ + --color-green: #a3c765; + --color-green-dark: #8ab34e; + --color-green-light: #e8f5d4; + --color-blue: #98b6fa; + --color-blue-dark: #7a9de8; + --color-navy: #5871ad; + --color-navy-light: #82A8FF; + --color-purple: #9B59B6; + --color-purple-dark: #7B2D8E; + --color-danger: #CC6958; + --color-danger-dark: #b5503f; + --color-check: #678731; + --color-cross: #c77765; + --color-gold: #d4a843; + + /* ---- Color: Surface ---- */ + --surface-base: #FFFFFF; + --surface-dim: #F9F9FA; + --surface-container: #F4F4F5; + --surface-container-high: #EEEEEF; + --surface-inverse: #0b0b0b; + + /* ---- Color: Text ---- */ + --text-primary: #0b0b0b; + --text-body: #515151; + --text-secondary: #666666; + --text-muted: #777777; + --text-faint: #999999; + --text-disabled: #BBBBBB; + --text-inverse: #FFFFFF; + --text-on-green: #FFFFFF; + --text-on-navy: #FFFFFF; + + /* ---- Color: Border ---- */ + --border-default: #e0e0e0; + --border-light: #eeeeee; + --border-focus: #5871ad; + --border-error: #CC6958; + + /* ---- Color: State overlays ---- */ + --state-hover: rgba(0, 0, 0, 0.04); + --state-focus: rgba(88, 113, 173, 0.12); + --state-pressed: rgba(0, 0, 0, 0.08); + --state-dragged: rgba(0, 0, 0, 0.12); + --state-disabled-bg: rgba(0, 0, 0, 0.04); + --state-disabled-text: rgba(0, 0, 0, 0.26); + + /* ---- Color: Alerts ---- */ + --alert-success-bg: #e8f5d4; + --alert-success-text: #155724; + --alert-success-border: #c3e6cb; + --alert-info-bg: #dce8ff; + --alert-info-text: #0c5460; + --alert-info-border: #bee5eb; + --alert-warning-bg: #fff3cd; + --alert-warning-text: #856404; + --alert-warning-border: #ffeeba; + --alert-danger-bg: #f8d7da; + --alert-danger-text: #721c24; + --alert-danger-border: #f5c6cb; + + /* ---- Typography ---- */ + --font-family: helvetica, arial, sans-serif; + --font-mono: 'SF Mono', 'Menlo', 'Consolas', monospace; + + /* Type scale — major third (1.250) */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-md: 1.125rem; /* 18px */ + --text-lg: 1.25rem; /* 20px */ + --text-xl: 1.5rem; /* 24px */ + --text-2xl: 1.875rem; /* 30px */ + --text-3xl: 2.25rem; /* 36px */ + --text-4xl: 3rem; /* 48px */ + --text-5xl: 3.75rem; /* 60px */ + + /* Line heights */ + --leading-none: 1; + --leading-tight: 1.15; + --leading-snug: 1.3; + --leading-normal: 1.5; + --leading-relaxed: 1.65; + --leading-loose: 1.8; + + /* Font weights */ + --weight-regular: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + + /* Letter spacing */ + --tracking-tight: -0.02em; + --tracking-normal: 0; + --tracking-wide: 0.02em; + --tracking-wider: 0.05em; + --tracking-widest: 0.08em; + + /* ---- Spacing scale (4px base) ---- */ + --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; + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-2xl: 24px; + --radius-full: 9999px; + + /* ---- Elevation (box-shadow) ---- */ + --elevation-0: none; + --elevation-1: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); + --elevation-2: 0 2px 6px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04); + --elevation-3: 0 4px 12px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.04); + --elevation-4: 0 8px 24px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04); + --elevation-5: 0 16px 48px rgba(0,0,0,0.10), 0 4px 12px rgba(0,0,0,0.05); + + /* ---- Motion ---- */ + /* Durations */ + --duration-instant: 50ms; + --duration-fast: 100ms; + --duration-normal: 200ms; + --duration-slow: 300ms; + --duration-slower: 400ms; + --duration-entrance: 250ms; + --duration-exit: 200ms; + + /* Easing — based on material curves */ + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-decelerate: cubic-bezier(0, 0, 0, 1); + --ease-accelerate: cubic-bezier(0.3, 0, 1, 1); + --ease-emphasize: cubic-bezier(0.2, 0, 0, 1); + --ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275); + + /* ---- Z-index scale ---- */ + --z-base: 0; + --z-dropdown: 100; + --z-sticky: 200; + --z-overlay: 300; + --z-modal: 400; + --z-toast: 500; + --z-ribbon: 600; + + /* ---- Layout ---- */ + --content-width: 800px; + --content-narrow: 400px; + --content-wide: 1200px; + --bp-tablet: 800px; + --bp-desktop: 1200px; +} + +/* ============================================ + BASE RESETS & DEFAULTS + ============================================ */ + +*, *::before, *::after { + box-sizing: border-box; +} + +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: var(--font-family); + font-size: var(--text-base); + font-weight: var(--weight-regular); + line-height: var(--leading-normal); + color: var(--text-body); + background-color: var(--surface-dim); + margin: 0; +} + +/* ============================================ + FOCUS — Accessible, visible, consistent + ============================================ */ + +:focus-visible { + outline: 2px solid var(--border-focus); + outline-offset: 2px; +} + +:focus:not(:focus-visible) { + outline: none; +} + +/* ============================================ + SELECTION + ============================================ */ + +::selection { + background: var(--color-navy-light); + color: var(--text-inverse); +} + +/* ============================================ + TYPOGRAPHY UTILITIES + ============================================ */ + +.type-display { + font-size: clamp(var(--text-3xl), 5vw, var(--text-5xl)); + font-weight: var(--weight-bold); + line-height: var(--leading-tight); + letter-spacing: var(--tracking-tight); + color: var(--text-primary); +} + +.type-headline-1 { + font-size: var(--text-4xl); + font-weight: var(--weight-bold); + line-height: var(--leading-tight); + letter-spacing: var(--tracking-tight); + color: var(--text-primary); +} + +.type-headline-2 { + font-size: var(--text-3xl); + font-weight: var(--weight-bold); + line-height: var(--leading-tight); + letter-spacing: var(--tracking-tight); + color: var(--text-primary); +} + +.type-headline-3 { + font-size: var(--text-2xl); + font-weight: var(--weight-bold); + line-height: var(--leading-snug); + color: var(--text-primary); +} + +.type-title-lg { + font-size: var(--text-xl); + font-weight: var(--weight-semibold); + line-height: var(--leading-snug); + color: var(--text-primary); +} + +.type-title { + font-size: var(--text-lg); + font-weight: var(--weight-semibold); + line-height: var(--leading-snug); + color: var(--text-primary); +} + +.type-title-sm { + font-size: var(--text-base); + font-weight: var(--weight-semibold); + line-height: var(--leading-normal); + color: var(--text-primary); +} + +.type-body-lg { + font-size: var(--text-md); + font-weight: var(--weight-regular); + line-height: var(--leading-relaxed); + color: var(--text-body); +} + +.type-body { + font-size: var(--text-base); + font-weight: var(--weight-regular); + line-height: var(--leading-relaxed); + color: var(--text-body); +} + +.type-body-sm { + font-size: var(--text-sm); + font-weight: var(--weight-regular); + line-height: var(--leading-relaxed); + color: var(--text-body); +} + +.type-label-lg { + font-size: var(--text-sm); + font-weight: var(--weight-semibold); + line-height: var(--leading-normal); + letter-spacing: var(--tracking-wide); + color: var(--text-primary); +} + +.type-label { + font-size: var(--text-xs); + font-weight: var(--weight-semibold); + line-height: var(--leading-normal); + letter-spacing: var(--tracking-wider); + text-transform: uppercase; + color: var(--text-muted); +} + +.type-caption { + font-size: var(--text-xs); + font-weight: var(--weight-regular); + line-height: var(--leading-normal); + color: var(--text-faint); +} + +.type-overline { + font-size: 11px; + font-weight: var(--weight-bold); + line-height: var(--leading-normal); + letter-spacing: var(--tracking-widest); + text-transform: uppercase; + color: var(--text-muted); +} + +.type-code { + font-family: var(--font-mono); + font-size: 0.9em; + background: var(--surface-container); + padding: 2px 6px; + border-radius: var(--radius-sm); +} + +/* ============================================ + ELEVATION UTILITIES + ============================================ */ + +.elevation-0 { box-shadow: var(--elevation-0); } +.elevation-1 { box-shadow: var(--elevation-1); } +.elevation-2 { box-shadow: var(--elevation-2); } +.elevation-3 { box-shadow: var(--elevation-3); } +.elevation-4 { box-shadow: var(--elevation-4); } +.elevation-5 { box-shadow: var(--elevation-5); } + +/* ============================================ + SURFACE UTILITIES + ============================================ */ + +.surface-base { background-color: var(--surface-base); } +.surface-dim { background-color: var(--surface-dim); } +.surface-container { background-color: var(--surface-container); } +.surface-inverse { background-color: var(--surface-inverse); color: var(--text-inverse); } + +/* ============================================ + INTERACTIVE STATE LAYER + Overlay that reacts to hover/focus/press. + Apply to any interactive element. + ============================================ */ + +.state-layer { + position: relative; + overflow: hidden; +} + +.state-layer::after { + content: ''; + position: absolute; + inset: 0; + background: currentColor; + opacity: 0; + transition: opacity var(--duration-fast) var(--ease-standard); + pointer-events: none; + border-radius: inherit; +} + +.state-layer:hover::after { + opacity: 0.04; +} + +.state-layer:focus-visible::after { + opacity: 0.08; +} + +.state-layer:active::after { + opacity: 0.10; +} + +/* ============================================ + RIPPLE EFFECT + Add .ripple to any interactive element. + Requires JS initialization (see styleguide). + ============================================ */ + +.ripple { + position: relative; + overflow: hidden; +} + +.ripple-wave { + position: absolute; + border-radius: 50%; + background: rgba(255, 255, 255, 0.35); + transform: scale(0); + animation: ripple-expand var(--duration-slower) var(--ease-decelerate) forwards; + pointer-events: none; +} + +@keyframes ripple-expand { + to { + transform: scale(2.5); + opacity: 0; + } +} + +/* Dark ripple for light backgrounds */ +.ripple-dark .ripple-wave { + background: rgba(0, 0, 0, 0.12); +} + +/* ============================================ + TRANSITION UTILITIES + ============================================ */ + +.transition-all { transition: all var(--duration-normal) var(--ease-standard); } +.transition-colors { transition: color var(--duration-normal) var(--ease-standard), background-color var(--duration-normal) var(--ease-standard), border-color var(--duration-normal) var(--ease-standard); } +.transition-shadow { transition: box-shadow var(--duration-normal) var(--ease-standard); } +.transition-transform { transition: transform var(--duration-normal) var(--ease-standard); } + +/* ============================================ + ENTRANCE ANIMATIONS + ============================================ */ + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fade-in-up { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fade-in-down { + from { opacity: 0; transform: translateY(-16px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fade-in-scale { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes slide-in-right { + from { opacity: 0; transform: translateX(24px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes slide-in-left { + from { opacity: 0; transform: translateX(-24px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes scale-in { + from { transform: scale(0); } + to { transform: scale(1); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.animate-fade-in { animation: fade-in var(--duration-entrance) var(--ease-decelerate) both; } +.animate-fade-in-up { animation: fade-in-up var(--duration-entrance) var(--ease-decelerate) both; } +.animate-fade-in-down { animation: fade-in-down var(--duration-entrance) var(--ease-decelerate) both; } +.animate-fade-in-scale { animation: fade-in-scale var(--duration-entrance) var(--ease-decelerate) both; } +.animate-slide-in-right { animation: slide-in-right var(--duration-slow) var(--ease-decelerate) both; } +.animate-slide-in-left { animation: slide-in-left var(--duration-slow) var(--ease-decelerate) both; } +.animate-pulse { animation: pulse 2s var(--ease-standard) infinite; } +.animate-spin { animation: spin 1s linear infinite; } + +/* Staggered delays for lists */ +.stagger-1 { animation-delay: 50ms; } +.stagger-2 { animation-delay: 100ms; } +.stagger-3 { animation-delay: 150ms; } +.stagger-4 { animation-delay: 200ms; } +.stagger-5 { animation-delay: 250ms; } +.stagger-6 { animation-delay: 300ms; } + +/* Respect reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +/* ============================================ + SKELETON LOADING + ============================================ */ + +.skeleton { + background: linear-gradient(90deg, + var(--surface-container) 25%, + var(--surface-container-high) 50%, + var(--surface-container) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: var(--radius-md); + color: transparent !important; +} + +.skeleton-text { + height: 1em; + margin-bottom: 0.5em; + border-radius: var(--radius-sm); +} + +.skeleton-circle { + border-radius: var(--radius-full); +} + +/* ============================================ + SPINNER + ============================================ */ + +.spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 2px solid var(--border-default); + border-top-color: var(--color-navy); + border-radius: var(--radius-full); + animation: spin 0.6s linear infinite; +} + +.spinner-sm { width: 14px; height: 14px; } +.spinner-lg { width: 32px; height: 32px; border-width: 3px; } + +/* ============================================ + DISABLED STATE + ============================================ */ + +[disabled], +.disabled { + opacity: 0.38; + pointer-events: none; + cursor: not-allowed; +} + +/* ============================================ + SCROLL-TRIGGERED REVEAL + Elements start hidden, JS adds .revealed + ============================================ */ + +.reveal { + opacity: 0; + transform: translateY(20px); + transition: opacity var(--duration-slow) var(--ease-decelerate), + transform var(--duration-slow) var(--ease-decelerate); +} + +.reveal.revealed { + opacity: 1; + transform: translateY(0); +} + +.reveal-scale { + opacity: 0; + transform: scale(0.96); + transition: opacity var(--duration-slow) var(--ease-decelerate), + transform var(--duration-slow) var(--ease-decelerate); +} + +.reveal-scale.revealed { + opacity: 1; + transform: scale(1); +} + +/* ============================================ + SPACING UTILITIES + ============================================ */ + +.mt-0 { margin-top: var(--space-0); } +.mt-1 { margin-top: var(--space-1); } +.mt-2 { margin-top: var(--space-2); } +.mt-3 { margin-top: var(--space-3); } +.mt-4 { margin-top: var(--space-4); } +.mt-6 { margin-top: var(--space-6); } +.mt-8 { margin-top: var(--space-8); } +.mt-10 { margin-top: var(--space-10); } + +.mb-0 { margin-bottom: var(--space-0); } +.mb-1 { margin-bottom: var(--space-1); } +.mb-2 { margin-bottom: var(--space-2); } +.mb-3 { margin-bottom: var(--space-3); } +.mb-4 { margin-bottom: var(--space-4); } +.mb-6 { margin-bottom: var(--space-6); } +.mb-8 { margin-bottom: var(--space-8); } +.mb-10 { margin-bottom: var(--space-10); } + +.p-0 { padding: var(--space-0); } +.p-2 { padding: var(--space-2); } +.p-4 { padding: var(--space-4); } +.p-6 { padding: var(--space-6); } +.p-8 { padding: var(--space-8); } + +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } +.gap-6 { gap: var(--space-6); } +.gap-8 { gap: var(--space-8); } + +/* ============================================ + RESPONSIVE CONTAINER + ============================================ */ + +.container { + width: 100%; + max-width: var(--content-width); + margin-left: auto; + margin-right: auto; + padding-left: var(--space-6); + padding-right: var(--space-6); +} + +.container-narrow { + max-width: var(--content-narrow); +} + +.container-wide { + max-width: var(--content-wide); +} diff --git a/make_post_sell/templates/base.j2 b/make_post_sell/templates/base.j2 index 706d182..6c381ab 100644 --- a/make_post_sell/templates/base.j2 +++ b/make_post_sell/templates/base.j2 @@ -17,7 +17,8 @@ - + + diff --git a/make_post_sell/templates/styleguide.j2 b/make_post_sell/templates/styleguide.j2 index ab0b8de..e49cadc 100644 --- a/make_post_sell/templates/styleguide.j2 +++ b/make_post_sell/templates/styleguide.j2 @@ -1,75 +1,163 @@ {% extends "base.j2" -%} {% block append_to_head_tag_section %} -Style Guide — Make Post Sell +Style Guide — Make Post Sell Design System {% endblock %} @@ -77,33 +165,124 @@
-

Style Guide

-

Live reference of every component in Make Post Sell. Toggle dark/light mode to see both themes.

+

Design System

+

+ Design tokens, components, and patterns for Make Post Sell. Built on CSS custom properties. Mobile-first. Accessible. Respects reduced motion. Toggle dark/light mode to see both themes. +

+ +
+ +
+
Design Tokens
+ +

All shared values live in tokens.css as CSS custom properties. Every component references tokens, never raw values. Change a token, change everything.

+ +
<!-- Include order --> +<link rel="stylesheet" href="/static/css/tokens.css"> /* tokens first */ +<link rel="stylesheet" href="/static/css/common.css"> /* app styles */ + +tokens.css provides: base resets, color/spacing/shape/motion/elevation tokens, +typography scale, animation utilities, skeleton/spinner, scroll-reveal, +prefers-reduced-motion support.
+ +
tokens.css is the foundation. common.css layers app-specific components on top and overrides dark mode via [data-theme="dark"] selectors.
+
+ + -
+
Colors
-
Light Mode
+
Brand (tokens.css)
-
Blue
#98b6fa
-
Green
#a3c765
-
Red
#bc2131
-
Purple
#9B59B6
-
Navy
#5871ad
-
Gray (base btn)
#D4D0C8
+
Green CTA
#a3c765
+
Green Dark
#8ab34e
+
Blue
#98b6fa
+
Navy
#5871ad
+
Navy Light
#82A8FF
+
Purple
#9B59B6
+
Danger
#CC6958
-
Dark Mode
+
Surfaces
+
+
Base
#FFFFFF
+
Dim
#F9F9FA
+
Container
#F4F4F5
+
Container High
#EEEEEF
+
Inverse
#0b0b0b
+
+
+ +
+
Text
+
+
Primary
#0b0b0b
+
Body
#515151
+
Secondary
#666666
+
Muted
#777777
+
Faint
#999999
+
Disabled
#BBBBBB
+
+
+ +
+
Status & Feedback
+
+
Check Green
#678731
+
Cross Red
#c77765
+
Gold
#d4a843
+
Warning BG
#fff3cd
+
Danger BG
#f8d7da
+
+
+ +
+
Borders
+
+
Default
#e0e0e0
+
Light
#eeeeee
+
Focus
#5871ad
+
Error
#CC6958
+
+
+ +
+
Dark Mode (common.css overrides)
Blue
#7ab9ff
Green
#08e700
@@ -113,37 +292,278 @@
Button BG
#2d3748
- -
-
Status & Feedback
-
-
Check Green
#678731
-
Cross Red
#c77765
-
Warning BG
#fff3cd
-
Danger BG
#f8d7da
-
-
+ -
+
Typography
-
-

Heading 1 (1.25em)

-

Heading 2

-

Heading 3

-

Body text. Helvetica, system default sizing. Color adapts to theme via CSS variables.

-

Link style — inherits theme link color

-

Bold textItalic text

+ +
+
Type scale (tokens.css — major third 1.250)
+
+
Display
+
Headline 1
+
Headline 2
+
Headline 3
+
Title Large
+
Title
+
Title Small
+
Body Large — ideal for landing pages and long reads.
+
Body — default for app interfaces and dashboard text.
+
Body Small — secondary information, metadata, timestamps.
+
Label Large
+
LABEL — SECTION HEADERS, GROUP TITLES
+
Caption — smallest text, footnotes
+
OVERLINE — CATEGORY TAGS
+
+
.type-display clamp(2.25rem, 5vw, 3.75rem) 700 tight +.type-headline-1 3rem (48px) 700 tight +.type-headline-2 2.25rem (36px) 700 tight +.type-headline-3 1.875rem (30px) 700 snug +.type-title-lg 1.5rem (24px) 600 snug +.type-title 1.25rem (20px) 600 snug +.type-title-sm 1rem (16px) 600 normal +.type-body-lg 1.125rem (18px) 400 relaxed +.type-body 1rem (16px) 400 relaxed +.type-body-sm 0.875rem (14px) 400 relaxed +.type-label-lg 0.875rem (14px) 600 wide +.type-label 0.75rem (12px) 600 UPPERCASE wider +.type-caption 0.75rem (12px) 400 normal +.type-overline 11px 700 UPPERCASE widest +.type-code monospace 0.9em inline code
+
New code should prefer .type-* classes from tokens.css over raw heading tags.
-
font-family: helvetica + +
+
Legacy app typography (common.css)
+
+

Heading 1 (1.25em)

+

Heading 2

+

Heading 3

+

Body text. Helvetica, system default sizing. Color adapts to theme via CSS variables.

+

Link style — inherits theme link color

+

Bold textItalic text

+
+
font-family: helvetica Headings: h1 = 1.25em, margin-top: 0 -Links: var(--link-color) in dark, #5f6368 in light +Links: var(--link-color) Body: var(--text-primary)
+
+ + +
+
Elevation
+ +

Five shadow levels from tokens.css. Higher = more prominent = closer to the viewer.

+ +
+
+
0
+
1
+
2
+
3
+
4
+
5
+
+
+
--elevation-0: none +--elevation-1: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04) /* cards at rest */ +--elevation-2: 0 2px 6px rgba(0,0,0,0.06), 0 2px 4px rgba(0,0,0,0.04) /* cards hovered */ +--elevation-3: 0 4px 12px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.04) /* dropdown, popover */ +--elevation-4: 0 8px 24px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04) /* modal, dialog */ +--elevation-5: 0 16px 48px rgba(0,0,0,0.10), 0 4px 12px rgba(0,0,0,0.05) /* toast, overlay */ + +Usage: element rests at N, hovers at N+1 or N+2 +Cards: rest 1, hover 3 | Buttons: rest 1, hover 2 | Modals: 4 | Toasts: 5
+
+ + + +
+
Motion
+ +
+
Easing curves
+
+
+
Standard
+
Decel
+
Accel
+
Spring
+
+
+
--ease-standard: cubic-bezier(0.2, 0, 0, 1) /* default for most transitions */ +--ease-decelerate: cubic-bezier(0, 0, 0, 1) /* entering elements (fade in, slide in) */ +--ease-accelerate: cubic-bezier(0.3, 0, 1, 1) /* exiting elements (fade out) */ +--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275) /* playful overshoot */ + +Hover each box above to feel the curve difference.
+
+ +
+
Durations
+
--duration-instant: 50ms /* micro-feedback (opacity toggle) */ +--duration-fast: 100ms /* state layer opacity changes */ +--duration-normal: 200ms /* color, shadow, border transitions */ +--duration-slow: 300ms /* transform transitions (scale, translate) */ +--duration-slower: 400ms /* complex animations (ripple) */ +--duration-entrance: 250ms /* element enters viewport */ +--duration-exit: 200ms /* element leaves (shorter = snappier) */
+
+ +
+
Entrance animations
+
+
+
fade-in
+
fade-up
+
scale
+
slide-R
+
+
+
.animate-fade-in .animate-fade-in-up .animate-fade-in-down +.animate-fade-in-scale .animate-slide-in-right .animate-slide-in-left +.animate-pulse .animate-spin +.stagger-1 through .stagger-6 (50ms increments) + +@media (prefers-reduced-motion: reduce) — all animations collapse to instant.
+
+
+ + + +
+
Spacing
+ +

4px base unit. Consistent spacing prevents the "off by 3px" disease.

+ +
+
+
 
+
2
+
3
+
4
+
5
+
6
+
8
+
10
+
12
+
16
+
20
+
24
+
+
+
--space-1: 4px --space-8: 32px +--space-2: 8px --space-10: 40px +--space-3: 12px --space-12: 48px +--space-4: 16px --space-16: 64px +--space-5: 20px --space-20: 80px +--space-6: 24px --space-24: 96px + +Utilities: .mt-{n} .mb-{n} .p-{n} .gap-{n}
+
+ + + +
+
Shape
+ +
+
+
none
+
sm 4px
+
md 8px
+
lg 12px
+
xl 16px
+
2xl 24px
+
full
+
+
+
--radius-none: 0 /* hard edges */ +--radius-sm: 4px /* inputs, code blocks, app buttons */ +--radius-md: 8px /* small cards, wells */ +--radius-lg: 12px /* feature cards */ +--radius-xl: 16px /* pricing cards */ +--radius-2xl: 24px /* large containers */ +--radius-full: 9999px /* pills, avatars, badges */ + +Rule of thumb: bigger element = bigger radius.
+
+ + + +
+
Interactive States
+ +

State layer: a semi-transparent overlay that responds to interaction. Apply .state-layer to any interactive element.

+ +
+
+
Hover me
+ +
+
+
.state-layer::after { + content: ''; position: absolute; inset: 0; + background: currentColor; opacity: 0; + transition: opacity 100ms ease; pointer-events: none; +} +:hover::after { opacity: 0.04; } +:focus-visible { opacity: 0.08; } +:active::after { opacity: 0.10; } + +Focus ring: :focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; } +Disabled: [disabled], .disabled { opacity: 0.38; pointer-events: none; }
+
+ + + +
+
Loading States
+ +
+
Skeleton
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
.skeleton shimmer gradient animation (1.5s loop) +.skeleton-text height: 1em, small radius +.skeleton-circle border-radius: full
+
+ +
+
Spinner
+
+
+ + + +
+
+
.spinner 20px, 2px border, navy top, 0.6s spin +.spinner-sm 14px +.spinner-lg 32px, 3px border
+
+
+ + -
+
Buttons
@@ -181,23 +601,44 @@ Dark mode: background-image patterns, colored borders
-
Small Buttons & Cancel
+
Small Buttons, Cancel & Disabled
+ New ☰ Actions + Disabled
-
.mps-button-small — compact (140px min-width at desktop) -.mps-cancel-button — inline cancel link -.mps-cancel-button-red — inline danger link
+
.mps-button-small — compact (140px min-width at desktop) +.mps-cancel-button — inline cancel link +.mps-cancel-button-red — inline danger link +.disabled / [disabled] — opacity 0.38, pointer-events none
+
+ +
+
Cart Action Buttons
+
+
+ + + + + +
+
+
.cart-checkout-button — green (#a3c765) +.cart-save-button — blue (#98b6fa) +.cart-activate-button — navy (#5871ad) +.cart-delete-button — danger (#CC6958, white text) +.cart-remove-link — bare link, no button chrome
+ -
+
Form Elements
@@ -211,14 +652,19 @@ Dark mode: background-image patterns, colored borders


+

+
+
input[type="text"] — padding: 8px, border-radius: 4px, 1px solid #ccc textarea — same styling, box-sizing: border-box -Dark mode: var(--input-bg), var(--input-border)
+.mps-submit — submit button +Dark mode: var(--input-bg), var(--input-border), var(--input-text)
+ -
+
Wells & Containers
@@ -234,38 +680,78 @@ Dark mode: var(--input-bg), var(--input-border)
Success container with green background.
+ +
+
Coupon (dotted border)
+
+ Coupon container with dotted border. border: 3px dotted #bbb, border-radius: 4px, padding: 20px. +
+
+ -
+
Alerts
-
Success alert
+
Success alert — --alert-success-bg
-
Info alert
+
Info alert — --alert-info-bg
-
Warning alert
+
Warning alert — --alert-warning-bg
-
Danger alert
+
Danger alert — --alert-danger-bg
-
.alert.alert-success — green bg (#a3c765) -.alert.alert-info — blue bg (#98b6fa) -.alert.alert-warning — yellow bg (#fff3cd) -.alert.alert-danger — red bg (#f8d7da)
+
Variants: .alert-success, .alert-info, .alert-warning, .alert-danger +Token pairs: --alert-{variant}-bg, --alert-{variant}-text, --alert-{variant}-border
+ + +
+
Status Text & Notices
+ +
+
Inline Status Colors
+
+

✓ Payment Confirmed

+

⏳ Awaiting Confirmations

+

⏳ Waiting for Payment

+

✕ Payment Expired

+
+
.status-success #28a745 +.status-warning #ffc107 +.status-info #6c757d +.status-error #dc3545
+
+ +
+
Status Notice Boxes
+
+
Warning notice — yellow background
+
Error notice — red background
+
Info notice — blue background
+
+
.status-warning-notice bg: #fff3cd, border: #ffeaa7 +.status-error-notice bg: #f8d7da, border: #f5c6cb +.status-info-notice bg: #d1ecf1, border: #bee5eb +Dark mode overrides via --notice-*-bg and --notice-*-border tokens.
+
+
+ + -
+
Product Cards (SERP)
-
Image
+
Image
Product Title
$19.99 @@ -273,7 +759,7 @@ Dark mode: var(--input-bg), var(--input-border)
Shop Name
-
Image
+
Image
Another Product
$7.50 @@ -287,25 +773,80 @@ Dark mode: var(--input-bg), var(--input-border)
.serp-thumbnail — border-radius: 4px, width: 100%
- -
-
Layout
-
.one-column — max-width: 600px, centered -.one-column-thin — max-width: 400px, centered -.action-columns — 2-column grid (50/50) -section.nav — mobile: stacked, desktop: 3 columns -section.content — main content area -Breakpoints: - @media (max-width: 640px) portrait — phone adjustments - @media (max-width: 768px) — tablet - @media (min-width: 800px) — desktop nav, 2-col layouts - @media (min-width: 960px) — wider product grids - @media (min-width: 1200px) — large desktop padding
+ +
+
Cart & Checkout Layout
+
Cart page: + section.cart-grid — stacked mobile, 4-col grid desktop (1fr 1fr 1fr 1fr) + section.cart-left — spans 3 columns + section.cart-left-grid — inner grid: 1fr 4fr 1fr (thumbnail | info | price) + section.cart-right — column 4, action buttons + .cart-shop-grid-span — spans 3 cols, shop name header + .cart-total-grid-span — spans 3 cols, right-aligned total + .cart-inline-form — display: inline (quantity + remove forms) + input.mps-quantity — narrow quantity input (60px) + img.product-cart-thumbnail — 60x60, border-radius 4px + +Checkout page: + section.checkout-page — grid, 1-col default, 2-col with :has(.checkout-left) + section.checkout-left — payment form + section.checkout-right — order summary + Mobile: collapses to single column at 768px
+ + +
+
Comment Actions
+
+ +
+
.reply-link — blue text, blue border on hover +.edit-link — same pattern +.delete-link — red text, red border on hover +.moderate-link — same as reply +Dark mode: uses theme background images on hover
+
+ + + +
+
Toggle / Details
+
+
+ Show API Keys +
+ Hidden content revealed on click. Pure CSS using <details> element. +
+
+
+
.toggle-summary — cursor: pointer, list-style: none, underline link +.api-key-details — margin-top: 10px +Uses native <details> element — no JS required.
+
+ + + +
+
Message Ribbon
+
+
+ Ribbon message here — gradient #5871ad to #82A8FF +
+
+
.message-ribbon — gradient banner, hidden on mobile, shown at 800px+ +Customizable per shop: ribbon_color_1, ribbon_color_2, ribbon_text_color
+
+ + -
+
Windows 95 Task Bar
@@ -322,10 +863,59 @@ Breakpoints: .call-to-action — right-aligned new button
+ + +
+
Layout
+ +
+
Containers
+
Legacy (common.css): + .one-column — max-width: 600px, centered + .one-column-thin — max-width: 400px, centered + .action-columns — 2-column grid (50/50) + +Token-based (tokens.css): + .container — max-width: var(--content-width) 800px, auto padding + .container-narrow — max-width: var(--content-narrow) 400px + .container-wide — max-width: var(--content-wide) 1200px
+
+ +
+
Breakpoints
+
Mobile first (no media query) +@media (max-width: 640px) — phone portrait adjustments +@media (max-width: 768px) — tablet +@media (min-width: 800px) — desktop nav, 2-col layouts +@media (min-width: 960px) — wider product grids +@media (min-width: 1200px) — large desktop padding + +Tokens: --bp-tablet: 800px, --bp-desktop: 1200px
+
+ +
+
Z-index scale
+
--z-base: 0 /* default */ +--z-dropdown: 100 /* dropdown menus */ +--z-sticky: 200 /* sticky headers */ +--z-overlay: 300 /* backdrop overlays */ +--z-modal: 400 /* modal dialogs */ +--z-toast: 500 /* toast notifications */ +--z-ribbon: 600 /* top ribbon banner */
+
+ +
+
CSS files
+
tokens.css — design tokens, base resets, utilities, animations +common.css — shared app styles (layout, buttons, nav, wells, footer, cards, dark mode)
+
+
+ + -
+
Theme System
-
CSS Variables (set on html[data-theme]): +
CSS Variables (common.css :root / [data-theme="dark"]): Light: Dark: --bg-primary: #ffffff --bg-primary: #0d1117 @@ -348,38 +938,9 @@ Dark buttons use background-image patterns: /static/img/trans-red.png (normal) / trans-red2.png (hover)
- -
-
Comment Actions
-
- -
-
.reply-link — blue text, blue border on hover -.edit-link — same pattern -.delete-link — red text, red border on hover -.moderate-link — same as reply -Dark mode: uses theme background images on hover
-
- - -
-
Message Ribbon
-
-
- Ribbon message here — gradient #5871ad to #82A8FF -
-
-
.message-ribbon — gradient banner, hidden on mobile, shown at 800px+ -Customizable per shop: ribbon_color_1, ribbon_color_2, ribbon_text_color
-
-
+
+ + +
+
Landing Page
+ +

The SaaS homepage for unauthenticated visitors. Uses design tokens throughout. Grid-based feature cards. Responsive: 3 columns → 2 → 1.

+ +
.landing-hero — centered hero, max-width: content-width, generous vertical margin +.landing-hero-sub — subtitle, constrained width, secondary text color +.landing-hero-ctas — inline grid of CTA buttons, stacks on mobile +.landing-features — 3-col grid of feature cards, responsive breakpoints +.landing-feature-card — well with icon + title + description, centered +.landing-feature-icon — large emoji, text-4xl +.landing-cta-bottom — bottom call-to-action well, centered
+ +
+
Login Card
+

The unified login/signup form. Centered card with elevation, styled input, and hint block.

+
.login-card — max-width 440px, centered, elevation-3, surface-base bg +.login-card-title — type-headline-3, centered +.login-card-subtitle — type-body, secondary color +.login-form — grid layout, gap-4 +.login-form-label — type-label, uppercase +.login-form-input — full-width, md radius, focus border animation +.login-form-submit — full-width mps-button-green +.login-card-hint — surface-dim background, explains what happens next
+
+
+ + + +
+
Class Act
+ +

Patterns that are thoughtfully designed and worth preserving. If something is a class act, it earned its place here through deliberate UX decisions, not accident.

+ +
+
Passwordless Unified Auth Flow
+ +

The sign-up and login flow is the same flow. There is no separate registration page. Every entry point — "Create a Shop," "Log In," "My Account" — routes to /join-or-log-in, which asks for one thing: your email.

+ +
+
+
+

Welcome

+

New and existing users — one step to get in.

+
+
Email address
+ +
Send verification code
+
+
+ What happens next? We email you a one-time code. Click it to log in — no password needed. +
+
+
+
+ +
+
Why this is a class act
+
DESIGN DECISIONS: + +1. One flow for new + existing users + No "Sign up" vs "Log in" split. The system figures it out. + New email? Account created. Known email? Welcome back. + Zero cognitive load for the visitor. + +2. Email-first, passwordless + No passwords to forget, leak, or brute-force. + A one-time verification code is emailed to the user. + Click the link or enter the code — done. + +3. Every CTA leads here + "Create a Shop," "My Account," "Log In," "Get Started" + — they ALL route to /join-or-log-in. + One URL. One form. No dead ends. + +4. Honeypot spam protection + A hidden email2 field catches bots. + No CAPTCHA friction for real humans. + +FLOW DIAGRAM: + + Visitor clicks any CTA + | + v + /join-or-log-in + [Enter email address] + | + v + Server checks email + | + +----+----+ + | | + New user Existing user + | | + Create Look up + account account + | | + +----+----+ + | + v + Email one-time code + | + v + User clicks link in email + | + v + Authenticated session + (redirect to ?next= or /) + +TEMPLATE: join-or-log-in.j2 +VIEW: views/user.py (join_or_log_in) +CSS: .login-card, .login-form-*
+
+
+ +
+
diff --git a/make_post_sell/tests/test_functional.py b/make_post_sell/tests/test_functional.py index 63105bb..3322429 100644 --- a/make_post_sell/tests/test_functional.py +++ b/make_post_sell/tests/test_functional.py @@ -102,12 +102,13 @@ class UnauthenticatedFunctionalTests(FunctionalTests): self.assertIn(b"Cart $0.00 (0)", res.body) def test_root_home_page_landing_content(self): - """Anonymous visitors see the MPS landing with tagline and signup CTA.""" + """Anonymous visitors see the MPS landing with hero, features, and CTAs.""" res = self.testapp.get("/", status=200) body = res.body.decode() - self.assertIn("Make. Post. Sell.", body) - self.assertIn("start selling digital products", body) + self.assertIn("Make it. Post it. Sell it.", body) + self.assertIn("Commission-free", body) self.assertIn("/join-or-log-in", body) + self.assertIn("/s/new", body) def test_new_product_redirects(self): redirect_res = self.testapp.get("/p/new", status=302) -- 2.49.1 From ef56f6ebc620c7be75053011bacd1d0cf57e50e3 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 6 Mar 2026 10:32:18 -0500 Subject: [PATCH 06/31] bump GIT_HASH to df65ed6 --- make_post_sell/GIT_HASH | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_post_sell/GIT_HASH b/make_post_sell/GIT_HASH index 44e13dc..dfb8a59 100644 --- a/make_post_sell/GIT_HASH +++ b/make_post_sell/GIT_HASH @@ -1 +1 @@ -1c8992a +df65ed6 -- 2.49.1 From e65e80c8b7d6efcdc1926cd549a46886a51aba0b Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 6 Mar 2026 10:41:06 -0500 Subject: [PATCH 07/31] fix: login card and landing page CSS specificity issues - Remove .well from login-card to prevent padding/bg override - Add border-radius and border directly to .login-card - Bump specificity on .landing-feature-card.well and .landing-cta-card.well - Constrain hero CTA button min-width to 180px - Add display:block to .login-form-input for reliable full-width --- make_post_sell/static/css/common.css | 35 +++++++++++++--------- make_post_sell/templates/join-or-log-in.j2 | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css index 26be9b5..2558c46 100644 --- a/make_post_sell/static/css/common.css +++ b/make_post_sell/static/css/common.css @@ -304,36 +304,40 @@ section.join-form { ============================================ */ .login-card { - max-width: 440px; - margin: var(--space-12) auto; - padding: var(--space-10) var(--space-8); + max-width: 420px; + margin: var(--space-10) auto; + padding: var(--space-8) var(--space-6); text-align: center; background-color: var(--surface-base); + border-radius: var(--radius-lg); + border: 1px solid var(--border-light); } .login-card-title { - margin-bottom: var(--space-2); + margin-bottom: var(--space-1); } .login-card-subtitle { - margin-bottom: var(--space-8); + margin-bottom: var(--space-6); color: var(--text-secondary); } .login-form { display: grid; - gap: var(--space-4); + gap: var(--space-3); text-align: left; } .login-form-label { - margin-bottom: calc(-1 * var(--space-2)); + margin-bottom: 0; } .login-form-input { + display: block; width: 100%; + min-width: 0; padding: var(--space-3) var(--space-4); - font-size: var(--text-md); + font-size: var(--text-base); border: 2px solid var(--border-default); border-radius: var(--radius-md); background-color: var(--input-bg); @@ -347,15 +351,14 @@ section.join-form { } .login-form-submit { - width: 100%; font-size: var(--text-base); - padding: var(--space-3) var(--space-6); + padding: var(--space-3) var(--space-4); cursor: pointer; } .login-card-hint { - margin-top: var(--space-6); - padding: var(--space-4); + margin-top: var(--space-4); + padding: var(--space-3) var(--space-4); background-color: var(--surface-dim); border-radius: var(--radius-md); text-align: left; @@ -385,6 +388,10 @@ section.join-form { justify-content: center; } +.landing-hero-ctas .mps-button { + min-width: 180px; +} + .landing-features { display: grid; grid-template-columns: repeat(3, 1fr); @@ -394,7 +401,7 @@ section.join-form { padding: 0 var(--space-6); } -.landing-feature-card { +.landing-feature-card.well { text-align: center; padding: var(--space-8) var(--space-6); } @@ -414,7 +421,7 @@ section.join-form { padding: 0 var(--space-6); } -.landing-cta-card { +.landing-cta-card.well { text-align: center; padding: var(--space-12) var(--space-8); } diff --git a/make_post_sell/templates/join-or-log-in.j2 b/make_post_sell/templates/join-or-log-in.j2 index 26693c1..ca7e20e 100644 --- a/make_post_sell/templates/join-or-log-in.j2 +++ b/make_post_sell/templates/join-or-log-in.j2 @@ -1,7 +1,7 @@ {% extends "base.j2" -%} {% block content -%}
-