A production-quality, responsive marketing site for Padeuce, recreated from the supplied design mockup. Dark premium sports-tech aesthetic: black and deep-green surfaces, a single neon yellow-green accent, dense data interfaces and large editorial typography.
Everything is static. There is no build step, no package manager and no runtime dependency — open it with any static file server and it runs.
- Technology
- Folder structure
- Running locally
- Editing the site
- Deployment
- Accessibility
- Browser support
| Area | Choice | Why |
|---|---|---|
| Markup | Semantic HTML5 | All copy lives in index.html and is editable without touching CSS or JS. |
| Styling | Modern CSS, Grid + Flexbox, custom properties | Layout is pure CSS; every design token is a variable in one file. |
| Scripting | Vanilla JavaScript (two small files) | Progressive enhancement only — the page is complete and navigable with JS disabled. |
| Icons | Local SVG sprite | One HTTP request, currentColor inheritance, no icon font or emoji. |
| Images | Local SVG assets | No hotlinking, no external requests, no layout shift. |
| Fonts | System font stack with a Space Grotesk / Sora / Manrope / Inter preference | Zero network cost; self-hosting is a one-line change (see below). |
Deliberately not used: React, Vue, Angular, Svelte, Bootstrap, Tailwind, jQuery, any CSS framework, any inline CSS or JS, and any build tooling.
Each file has one job, loaded in this order (order matters — later files override earlier ones):
| File | Responsibility |
|---|---|
reset.css |
Browser normalisation. No design decisions. |
variables.css |
Every design token: colour, spacing, radius, type scale, motion. |
base.css |
Document defaults, typography, text utilities, focus, a11y helpers. |
layout.css |
Containers, section rhythm, grid and split primitives. |
components.css |
Reusable pieces: header, buttons, cards, devices, dashboards, tables. |
sections.css |
Composition rules unique to one band of the page, ordered top-to-bottom. |
responsive.css |
All breakpoint overrides, in one place. |
/
├── index.html All page copy and structure
├── favicon.svg
├── README.md
├── assets/
│ ├── fonts/ Drop self-hosted woff2 files here (optional)
│ ├── icons/
│ │ └── icons.svg SVG sprite — every icon on the site
│ └── images/
│ ├── dashboards/ (reserved for dashboard screenshots)
│ ├── devices/ (reserved for device imagery)
│ ├── hero/
│ │ └── padel-court-night.jpg
│ ├── logos/
│ │ └── padeuce-mark.svg
│ └── placeholders/
│ └── avatar.svg
├── css/
│ ├── reset.css
│ ├── variables.css
│ ├── base.css
│ ├── layout.css
│ ├── components.css
│ ├── sections.css
│ └── responsive.css
└── js/
├── main.js Drawer, scroll spy, reveals, tabs, anchor focus
└── navigation.js Sticky header state
Any static server works. Pick one:
# Python 3 (no install needed on macOS/Linux)
python3 -m http.server 4173
# Node
npx serve .
npx http-server -p 4173
# PHP
php -S localhost:4173
# Ruby
ruby -run -e httpd . -p 4173Then open http://localhost:4173.
Opening
index.htmldirectly withfile://mostly works, but the SVG sprite (<use href="…">) is blocked by some browsers' file-origin rules, so icons may not appear. Use a server.
index.html is commented with a numbered banner before every section:
<!-- ====================================================================
8. TOURNAMENTS
==================================================================== -->Section IDs, used by the navigation and for deep links:
home, problem, opportunity, platform, players, profile,
tournaments, live, ecosystem, network, why, commercial, scale,
contact.
Edit css/variables.css only. To change the accent across the entire site:
:root {
--color-accent: #dfff00; /* primary neon */
--color-accent-bright: #edff38; /* hover state */
--color-accent-soft: rgba(223, 255, 0, 0.12);
--color-accent-border: rgba(223, 255, 0, 0.35);
}The surface ramp (--color-black → --color-surface-soft) controls how dark the
sections are, and --section-space controls the vertical rhythm of the whole page.
Replace assets/images/logos/padeuce-mark.svg with your own square mark (keep
the viewBox square). It is referenced twice — in the header and the footer:
<img class="logo__mark" src="assets/images/logos/padeuce-mark.svg" alt="" width="32" height="32">
<span class="logo__word">Padeuce</span>The wordmark is plain text, so edit it in place. favicon.svg is a copy of the
same mark — replace it too.
Swap the file and nothing else:
<img src="assets/images/hero/padel-court-night.jpg"
alt="A blue padel court at night, lit by a floodlight above the perimeter fence"
width="1535" height="1024" fetchpriority="high">Update width/height to the new file's real pixel size so the
browser reserves the right space. A photograph (.jpg/.webp) works exactly the
same way — the darkening is applied by .hero__overlay in CSS, not baked
into the file, so any image will pick up the correct treatment. Adjust
object-position in css/sections.css if the crop needs nudging.
The header carries only the logo and the section-drawer toggle — there is no primary nav menu. All section navigation happens through the strip below the hero and the drawer (see below).
Footer columns are plain lists; add or remove <li> items inside
.footer-nav__list.
The page has two linked navigators, both driven by the same code in main.js:
| Element | Where | Behaviour |
|---|---|---|
.section-nav |
Fixed bar docked to the bottom of the viewport | Chip rail with prev/next step buttons and a progress rule |
.deck-drawer |
Fixed panel on the right | Toggled by .deck-toggle in the header; open state remembered in localStorage |
Both highlight the current section as you scroll, and stay in sync — the strip defines the running order, and each stop collects every link pointing at it.
The two lists must be kept in sync. To add a stop, add one <li> to the
strip and one to the drawer, both pointing at the same id:
<!-- strip -->
<li><a class="section-nav__chip" href="#pricing"><span class="section-nav__index">15</span><span class="section-nav__label">Pricing</span></a></li>
<!-- drawer -->
<li><a class="deck-drawer__link" href="#pricing"><span class="deck-drawer__index">15</span>Pricing</a></li>The index numbers mirror each section's eyebrow number, so update the eyebrow
too. The 01 / 14 counter and the progress rules calculate themselves.
Drawer specifics:
- Width is one value:
--drawer-widthon.deck-drawer(250px, narrower on mobile). - Closed, the panel is
visibility: hidden, so its links leave the tab order. - Escape closes it and returns focus to the header toggle.
- Tapping a link closes it below 768px, where the panel covers the content.
- It sits at
z-index: 55— over the page, under the fixed header, so the header stays usable while the drawer is open.
The fixed chrome is sized from tokens in variables.css: --header-inset
and --header-bottom position the header, --section-nav-height /
--section-nav-bottom size the docked bar (the latter adds clearance and the
iOS safe-area inset), and --drawer-top hangs the drawer below the header.
body gets --section-nav-bottom as bottom padding so the footer clears the
bar, and scroll-padding-top / -bottom keep jumped-to sections clear of both.
Every card grid uses the same component. Copy one list item and edit the text:
<li class="card">
<span class="icon-chip">
<svg class="icon" aria-hidden="true"><use href="assets/icons/icons.svg#icon-club"></use></svg>
</span>
<span class="card__label">Clubs</span>
<h3 class="card__title">Manual admin</h3>
<p class="card__text">Short supporting sentence.</p>
</li>The grid reflows automatically — .grid--3 is three columns on desktop, two on
tablet and one on mobile.
In section 13, copy one <li class="card card--numbered card--revenue"> block
and update the number, badge, title and description:
<li class="card card--numbered card--revenue">
<span class="card__topline">
<span class="card__number">07</span>
<span class="badge badge--accent">Recurring</span>
</span>
<h3 class="card__title">New stream</h3>
<p class="card__text">What it is and who pays for it.</p>
</li>Use badge--accent for emphasis and plain badge for a muted label.
In section 10, copy one .matrix__cell. The matrix is a gap-based grid, so the
hairline borders redraw themselves:
<div class="matrix__cell">
<svg class="icon" aria-hidden="true"><use href="assets/icons/icons.svg#icon-plug"></use></svg>
<h3 class="matrix__title">Integrations</h3>
<p class="matrix__text">One short line.</p>
</div>The dashboards are real markup, not images. The building blocks are
.dashboard-frame → .dashboard-header → .dashboard-body → .dashboard-grid
→ .dashboard-panel, filled with .data-list, .metric, .progress-bar,
.bracket and .status-chip.
To change a row, edit the text:
<div class="data-list__row">
<span class="data-list__label"><span class="rank-index">1</span><span>Ferreira / Duarte</span></span>
<span class="data-list__value">5.41</span>
</div>Progress bars and the rating dial take their value from a meter class, not inline CSS:
<span class="progress-bar__track"><span class="progress-bar__fill meter-75"></span></span>
<div class="dial meter-72">…</div>Available steps are every 5% from meter-0 to meter-100, plus the exact values
the page uses. Need another? Add one line to the meter scale in
css/components.css:
.meter-83 { --meter: 83%; }The hero uses a supplied vector composition rather than markup:
<img class="device-art" src="assets/images/devices/Hero-Device.svg"
alt="…" width="592" height="981" fetchpriority="high">Swap the file to change it, and keep the alt text describing what the screen
shows — the artwork's own text is outlined, so a screen reader has nothing else
to go on. --device-art-width caps how wide it renders.
The players section handset is still built from the .device-phone
component. Swap the copy inside .score-display, .set-strip, .stat-grid,
.player-row and .progress-bar — no CSS changes required:
<div class="score-display__side score-display__side--serving">
<span class="score-display__points">5</span>
<span class="score-display__team">Ferreira / Duarte</span>
</div>--serving marks the side whose score renders in the accent colour.
All icons live in assets/icons/icons.svg as <symbol> elements sharing a
24×24 viewBox and 1.5 stroke width. Reference one by id:
<svg class="icon" aria-hidden="true"><use href="assets/icons/icons.svg#icon-trophy"></use></svg>To add an icon, append a new <symbol id="icon-name" viewBox="0 0 24 24"> to the
sprite. Icons inherit currentColor, so colour them by styling the parent.
Drop .woff2 files into assets/fonts/ and uncomment the @font-face block at
the top of css/base.css. The --font-sans stack already lists the preferred
families first, so nothing else changes.
The site is plain static files — upload the repository root as-is.
# Netlify
netlify deploy --prod --dir .
# Vercel
vercel --prod
# Cloudflare Pages
wrangler pages deploy .
# GitHub Pages — push to the repo, then enable Pages on the branch root
# S3
aws s3 sync . s3://your-bucket --exclude ".git/*" --exclude ".claude/*"No build command. No output directory. Set the publish directory to the project
root and the entry point to index.html.
Before going live, update the absolute URLs in the <head>: canonical,
og:url and the og:image / twitter:image paths.
Targets WCAG 2.2 AA:
- Skip-to-content link as the first focusable element.
- Semantic landmarks (
header,nav,main,footer) and labelled<nav>s. - One
<h1>; section headings descend in order without skipping levels. - Visible two-tone focus ring on every interactive element; focus is never removed.
- The section drawer's toggle manages
aria-expanded/aria-controls, closes on Escape, restores focus to the toggle, and hides its panel withvisibility: hiddenso closed links leave the tab order. - Profile tabs implement the ARIA tabs pattern with roving
tabindexand Arrow/Home/End key support. - Icon-only controls carry
.visually-hiddentext labels; decorative SVGs arearia-hidden="true". - The comparison table uses
<caption>,<th scope="col">and<th scope="row">, and every cell pairs its icon with a text label — no meaning is carried by colour alone. - All interactive targets are at least 44×44px.
prefers-reduced-motion: reducedisables animation, transitions and smooth scrolling; reveal animations never hide content when they are suppressed.- Anchor navigation moves keyboard focus to the destination section.
With JavaScript disabled the page renders and navigates completely: content is never hidden by default, and the section strip below the hero remains a working list of anchor links. (The drawer needs JS to open, which is why the strip carries the same 14 destinations.)
Modern evergreen browsers — Chrome/Edge 105+, Firefox 110+, Safari 15.4+.
The site relies on CSS custom properties, Grid, Flexbox, clamp(), aspect-ratio
and :focus-visible. Two effects degrade gracefully rather than breaking:
backdrop-filteron the header — falls back to a solid translucent panel.text-wrap: prettyandsvhunits — ignored by older engines with no layout impact.
No horizontal scrolling occurs at any width from 360px upward; dense dashboards and the comparison table scroll inside their own containers instead.