Brand Colors
Primary Palette
#00aeef
#0090c6
#40c2f3
#80d7f7
#cceffc
Bootstrap Semantic Colors
.bg-primary
.bg-secondary
.bg-success
.bg-danger
.bg-warning
.bg-info
.bg-light
.bg-dark
Text & Background Utilities
Badges
Alerts
Inputs & Form Controls
Select2 Dropdowns
.select-2 class lagao —
panel.js
automatically Select2 initialize kar deta hai Bootstrap 5 theme ke saath.
<select class="select-2" data-placeholder="Select...">
Forms
Login Form
Form with Validation
Modals
Tooltips & Popovers
Bootstrap Tooltips
Tippy.js
Popovers
Progress Bars
Spinners & Loaders
Border Spinners
Grow Spinners
Sizes
Inside Buttons
Full Page Overlay
Cards
Card with a colored header and footer.
John Doe
Product Designer
Task Progress
In ProgressWebsite redesign milestone Q2 2026.
Accordion
select-2 to
any
<select> element. panel.js auto-initializes it with Bootstrap 5
theme and
full search functionality. Supports single, multiple, and grouped
options.swalWithBootstrapButtons.fire({...}) for styled alerts. The
swalWithBootstrapButtons mixin is defined in panel.js and
applies
Bootstrap button styles automatically.
data-fancybox="gallery" on
anchor tags for image galleries. For inline HTML popups use
Fancybox.show([{ src: '#id', type: 'inline' }]). Bind:
Fancybox.bind('[data-fancybox]').
Tabs & Pills
Overview content — general summary of the selected item.
Details panel — specific information, specs, or logs.
Settings panel — configure preferences and options.
Showing all records.
Showing only active records.
Showing archived records.
Tables
User List
| # | Name | Role | Status | Progress | Actions | |
|---|---|---|---|---|---|---|
| 1 |
A
Alice Johnson
|
Admin | Active |
90%
|
|
|
| 2 |
B
Bob Smith
|
Editor | Pending |
55%
|
|
|
| 3 |
C
Carol White
|
Viewer | Inactive |
20%
|
|
|
| 4 |
D
David Brown
|
Editor | Active |
75%
|
|
|
| 5 |
E
Eve Davis
|
Admin | Review |
40%
|
|
SweetAlert2 (swalWithBootstrapButtons)
swalWithBootstrapButtons —
Bootstrap 5 styled buttons, custom animations. Swal.fire() directly
use mat
karo.
Fancybox — Image Gallery & Popups
Flatpickr — Date & Time Pickers
Language Switcher
Live component — English current, Italian current
EN
IT
Rules
- Show the language the visitor is on, never the one they would switch to. A bare “EN” sitting in the corner of an Italian page reads as a label of that page to about half the people who see it. The control states the current language and opens onto the others.
- Rectangular flag, then the ISO code. Flags are
served from
https://assetscache.eurekabike.com/flags/, the estate’s own asset host — the same one the admin panel uses. English is shown with the United States flag (us.svg), matching that panel; do not substitutegb.svg. - Build it on
<details>/<summary>. It works with JavaScript disabled and is announced as a disclosure by assistive technology. Script is only needed to close it on an outside click or on Escape. - Link to the twin page, not to the site root. File
names are localised across our sites (
ecosystem.html/ecosistema.html), so the alternate must be mapped per route. Dropping a reader on the other language’s home page loses their place; pointing at a name that does not exist there gives them a 404. - Never hide it on small screens. Below 900px the navigation links collapse, which leaves the switcher as the only way to change language. It stays.
On narrow screens, the endorser gives way
A phone cannot fit a logo, a language switcher and a call to action on one row. Something has to yield, and it is not the switcher.
On a product site the navigation keeps the product
name and drops by EurekaBike: document 04 places the endorser
“bottom right on cover pages, footer on web”, and
the footer still carries it. Where the lockup is the
site’s own mark — as on home.eurekabike — it never drops, it
scales. Draw that line with a sibling selector rather than a per-site override,
so the distinction is structural: a lockup preceded by a product wordmark is a
signature, a lockup standing alone is the subject.
Markup
<details class="langsel">
<summary aria-label="English — change language">
<img class="langsel-flag" src="…/flags/us.svg" alt="" width="21" height="14">
<span class="langsel-code">EN</span>
<span class="langsel-caret" aria-hidden="true"></span>
</summary>
<ul class="langsel-menu">
<li><a href="…/ecosistema.html" hreflang="it">
<img class="langsel-flag" src="…/flags/it.svg" alt="" width="21" height="14">
<span class="langsel-code">IT</span>
<span class="langsel-name">Italiano</span>
</a></li>
</ul>
</details>
/* narrow screens: the endorser yields, the subject scales */
@media (max-width: 640px) {
.nav-logo .wordmark ~ .tagline,
.nav-logo .wordmark ~ .eb-lockup { display: none; }
.langsel { flex-shrink: 0; } /* never hidden */
}
Open Graph Cards
Live templates, drawn at half scale — a product site and a brand site
HERMES
Il marketplace B2B della bike industry
Product site — the name leads, EurekaBike endorses
Il supersystem della bike industry
Brand site — the lockup is the subject, so BIKE keeps its azure
Three slots, and nothing else
- Nothing on the card may be smaller than 38px. LinkedIn renders a 1200px card at roughly 500, X at 506 — about 42%. An 11px eyebrow arrives at 5px and a 25px line of body copy at 10px: present in the file, illegible in the feed. 38px lands at 16px, which reads. Size the card by what survives the shrink, not by what looks balanced at 100%.
- The name, one payoff, the logo. That is the whole card. No eyebrow, no navigation, no second line of argument. If a payoff needs two lines, break it balanced rather than fill-and-wrap — a lone trailing word reads as a layout mistake, not a decision.
- Do not print the domain. Facebook, LinkedIn and X
print it themselves underneath the card, reading it from
og:url. Setting it in the artwork is not merely small, it is duplicated. - Light ground, left aligned, artwork bleeding right. This is the chapter-cover language of this playbook: the line illustration runs off the right edge and is put out by a single horizontal veil where the text column begins. Keep the veil a smooth curve — a straight two-stop ramp shows its own change of slope as a visible vertical seam.
- Fade the illustration before the veil, not only with it. At full strength the drawing eats the card. On the covers it is scenery, not subject; blend it about halfway to the ground first.
Which illustration, and which part of it
There are two drawings, and the choice is not
decorative. Properties of the bicycle vertical —
home.eurekabike, HERMES, AGORA, KOSMOS, EMPORION — use the
dedicated EurekaBike illustration, where the subject is the bike. The master
brand at eurekago.it keeps the generic playbook cover, where the
subject is the enterprise in the abstract.
Every site frames a different part of the same drawing. With one shared crop the previews become wallpaper: at feed scale five sites are indistinguishable, because the visible band is always the same object. Choose the fragment that argues for that product — cassette and shoes for product discovery, archive and chain for data — and enlarge the drawing enough to have room to pan. Mirroring it is allowed: the band sits on the right, so anything at the far left of the original is otherwise unreachable, and there is no type inside the drawing to reverse.
The endorsement follows document 04
- 50 to 60% of the name height. On the card above that is 60px against a 112px name — 54%.
- Ink on light, white on dark, never the vertical’s
primary colour. As an endorser the lockup is monochrome, so
BIKE loses its azure. Where the lockup is the
subject of the card —
home.eurekabike— the azure stays, because there it is not endorsing anything. - Use the real asset, do not retype the logo. The wordmark is a file in this repository. A name set in Archivo 900 is close enough to look intentional and wrong enough to be noticed.
- Drop the word by. At feed scale it is unreadable, and an unreadable word is worse than none: the size hierarchy and the accent rule already say which mark is the signature.
Technical specification
- 1200 × 630. The one size Facebook,
LinkedIn, WhatsApp and Slack all read, and which X accepts as
summary_large_image. - JPEG, quality 88, under 300 KB. WhatsApp discards heavier previews. PNG is the wrong container once the illustration is in the card — the same artwork lands around 420 KB as a PNG and about 85 KB as a JPEG, and on a near-bitonal line drawing the difference is not visible. A flat card of colour fields and type is the exception: that one stays PNG.
- Both languages, always. One card per language per property, with the payoff actually translated. A shared card means half the audience gets the wrong language at the moment the link is shared.
- Version the file name when the card changes. Crawlers cache previews hard, and republishing to the same URL leaves the old image in circulation for days.
Markup
<meta property="og:type" content="website">
<meta property="og:url" content="https://hermes.eurekabike.it/site/">
<meta property="og:title" content="HERMES — Il marketplace B2B della bike industry">
<meta property="og:description" content="…">
<meta property="og:locale" content="it_IT">
<meta property="og:locale:alternate" content="en_GB">
<!-- Absolute URL. A relative one resolves for the browser and fails for the crawler. -->
<meta property="og:image" content="https://hermes.eurekabike.it/site/og-image.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:type" content="image/jpeg">
<meta property="og:image:alt" content="HERMES, by EurekaBike">
<meta name="twitter:card" content="summary_large_image">
A website is not finished without it
A site counts as done only when every indexable page, in
every language, resolves an og:image that returns 200
at an absolute URL, with og:image:width and
og:image:height declared. The 404 page is the one exemption: it is
noindex, and nobody shares a 404. A missing card is not a cosmetic gap:
the platforms fall back to whatever image they find first on the page, or to
no image at all, and the link goes out looking abandoned.
Put the check in the builder, not on a list somebody is supposed to remember. The build must fail when the card is absent, is not 1200 × 630, or is over 300 KB — the same place that already fails on an empty render. Verify the deployed result with each platform’s own debugger before calling it shipped, because a card that is correct in the repository and 404 on the server looks identical from here.
Cookie Consent
One component, every property
- There is exactly one consent banner in the estate, and it
is parameterised. Storage key, copy, policy link and button labels
all arrive as
data-attributes on the<script>tag, so the same file ships everywhere and only the strings change. Do not fork it for a new site. Two copies of one component drift, and the drift shows up as a visual difference nobody chose. - Scope every rule to the banner’s own id. The
component injects its stylesheet into the document, so a bare
button { … }rule inside it restyles every button on the page. - Accept is solid azure, Decline is a bordered ghost. Both on the ink panel. The accepting action is the primary one and looks it; declining is equally reachable and equally legible, which is the point of the pairing — not a dark pattern where one option is hidden in low contrast.
Focus goes to the dialog, never to Accept
When the banner opens, move focus to the dialog
container (tabindex="-1"), not to the Accept button.
Focusing the button leaves a 3px azure ring drawn around it before
the visitor has touched anything. It is not a stray border in a stylesheet:
:focus-visible is a browser heuristic, and on a
programmatic focus some browsers decide the ring is warranted and some
do not. That is why identical code looked ringed on one property and clean on
another, and why chasing it in the CSS finds nothing.
Focusing the container keeps what the ring was there for — a keyboard user reaches the buttons with one Tab instead of traversing the whole page — while nothing is pre-highlighted. The keyboard focus ring itself stays. It is an accessibility requirement, not decoration, and must never be removed to tidy the look.
The banner, the tag and the policy are one switch
Whether analytics runs is a single setting, and the banner and the privacy policy both derive from it. Never write the analytics section of a policy by hand, and never ship a consent banner unconditionally.
Both halves of getting this wrong have already shipped, in opposite directions. Three sites declared “we use Google Analytics 4” and showed a consent banner while the analytics id was empty and no tag was ever emitted — asking permission for something that does not happen, and describing a processing activity that does not exist. A fourth had the mirror defect: the tag on every page, and a policy stating there was no analytics at all.
Neither is cosmetic. A privacy policy is a legal statement about what the software does, so the software is what has to be asked — not the person writing the copy, months earlier, in another language.
And the builder checks it
- The policy declares which branch it rendered,
in a machine-readable marker:
<!-- analytics-declared: yes|no -->. Do not have the check read prose — the first version of ours searched the text for “Google Analytics”, but the sentence that denies analytics contains those same two words, so it could not tell a claim from a denial and stayed silent in exactly the case it existed to catch. A marker is also language-independent, which matters on a bilingual estate. - The build fails when a tag ships with no banner, when a banner ships with no tag, when the tag is missing from some pages but not others (consent cannot be honoured on the pages that lack it), or when the marker disagrees with what was emitted. 404 is exempt: it is noindex and reached by accident, not chosen.
Behaviour
- Consent Mode v2 defaults to denied, set inline in
<head>before the GA4 config call. The banner only records a choice and, on accept, updates consent. Nothing in it loads a tag. - A decision already on record means no banner, and no second chance to change it there. Changing a decision belongs on the privacy page, not in a popup that reappears.
role="dialog", an accessible label, native buttons. Never a<div>with a click handler.