FlytBase-26 Design SystemLegacy format
The operating system for drone autonomy, expressed in the interface.
Typography
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Every letter tells a story worth reading, and every typeface gives that story a new voice waiting to be heard.
Spacing
Base: 8pxThe operating system for drone autonomy, expressed in the interface.
Components
Buttons
Cards
Card Title
Sample body text for the card component.
Eyebrow
Default
Green
Input
Default
Focused
Auto-cycling Tab Strip
Active Tab
Elevation & Depth
Do's & Don'ts
Do
Don't
Icons
Material IconsLibrary: Material Icons · Style: solid · Size: 40px
Table of contents
1. Brand voice and attitude
2. Color system
3. Typography
4. Spacing, layout and grid
5. Shape language
6. Component library
7. Patterns and textures
8. Motion
9. Imagery and iconography
10. Content and copy
11. Accessibility
12. Tech baseline
13. Token reference (Tailwind v4)
14. Hard rules (never break)
---
1. Brand voice and attitude
FlytBase is the autonomy layer for commercial drone operations. The brand reads as:
• Aerial: views are from altitude. The palette, gridlines, and horizon rules all come from looking down at oil and gas, mines, ports, roads, rail, and green terrain.
• Autonomous: the interface confirms work that already happened. Motion signals cause and effect. Nothing dances for attention.
• Industrial: sharp corners, monospace metadata, signed telemetry. No rounded corners. No drop shadows for decoration.
• Editorial where it matters: Lora italic carries the warmth. It appears in headline accents and welcoming moments. Everywhere else, stay structural.
Write as an operator would speak: short sentences, declarative, specific numbers. Avoid hype language ("revolutionary", "cutting-edge", "next-gen").
2. Color system
One brand accent, a neutral base, and a separate functional status layer. Brand colors express identity; status colors are usability signals — the two systems are kept apart (big-system convention) so a "success" green can sit anywhere without reading as a brand accent. See §2.7 for the status layer.
### 2.1 Signal orange (the sole brand accent)
The "drone detected something" color. High energy on dark backgrounds. Reads at small sizes.
| Token | Hex | Use |
|---|---|---|
| o50 | #FDF0E8 | Light tinted surfaces, pill backgrounds on light |
| o100 | #F4B896 | Highlights, accent text on dark |
| o200 | #EC7D42 | Hover state for o400, chip text |
| o400 | #D95B28 | Primary brand orange. CTAs, eyebrows, dot accents |
| o600 | #A33D14 | Pressed state, deeper accent |
| o800 | #7A2B0E | Thermal gradient mid-tone |
| o900 | #4A1808 | Thermal gradient deep tone |
### 2.2 Eucalyptus (success status — not a brand accent)
Cool blue-green. No longer a secondary brand accent — it now backs the success status only (§2.7). The e* scale stays as the primitive that --color-success aliases; treat green as a functional signal (done / confirmed / positive), never as a sectional brand accent.
| Token | Hex | Use |
|---|---|---|
| e50 | #EAF0EE | Success tint surface (light) |
| e100 | #BACDD0 | Subtle highlights |
| e200 | #72A899 | Success text / chip on dark |
| e400 | #3A7A65 | Primitive behind `success`. Confirmations, completed states |
| e600 | #2B5A4A | Hover / border tone |
| e800 | #1B3A30 | Deep tone |
| e900 | #0F2219 | Deepest tone |
### 2.3 Charcoal (dark base)
The default surface. Site background. Dark mode never toggles, this is the canonical look.
| Token | Hex | Use |
|---|---|---|
| dbg | #1A1A1A | Page background |
| ds | #242424 | Surface / card |
| de | #2E2E2E | Elevated surface, toggle track |
| db | #3D3D3D | Borders, dividers |
| dm | #8A8A8A | Muted text, meta-labels, hints (lifted from #5C5C5C so it clears WCAG AA on dbg; see §2.6) |
| dsc | #999999 | Subtle / secondary text |
| dbd | #D6D6D6 | Body text |
| dh | #F0F0F0 | Headings / high-contrast text |
### 2.4 Cool gray (light base)
Used whenever a section inverts to light. Pairs with the orange brand accent (the sole accent; light sections no longer use a green accent).
| Token | Hex | Use |
|---|---|---|
| lbg | #F5F5F7 | Light page / section background |
| ls | #FFFFFF | Light surface / card |
| lb | #DDDFE3 | Borders on light |
| lsc | #555555 | Body text on light |
| lp | #111111 | Headings on light |
### 2.5 Brand vs. status colors (hard)
> One brand accent: Signal Orange. Any green / red / amber / blue is a functional status, never a brand color.
The old "orange and green never share a viewport" separation rule is retired — green is no longer a brand accent, so a success checkmark or a progress chip can appear in any section, including orange ones. The replacement rule: never introduce a second brand accent, and never use orange to signal status (orange is brand-only). Status colors (§2.7) are functional, context-independent, and always paired with an icon + label, so they never compete with the brand.
### 2.6 Text contrast hierarchy (dark base, hard)
Text must stay legible and layered. Every text token is dim enough to sit below the one above it, but light enough to clear WCAG AA (≥4.5:1) on dbg (#1A1A1A). Measured ratios:
| Role | Token | Hex | Contrast on dbg | Use |
|---|---|---|---|---|
| Heading / high-contrast | dh | #F0F0F0 | ~15:1 | Headlines, key figures |
| Body | dbd | #D6D6D6 | ~12:1 | Default prose |
| Secondary | dsc | #999999 | ~6.1:1 | Supporting copy, captions, card descriptions |
| Muted / metadata | dm | #8A8A8A | ~5:1 | Eyebrows' meta-labels, hints, timestamps |
> Rule: never use a text color darker than dm (#8A8A8A) on dbg. Anything dimmer (db, de) is for borders / decoration only, never text. The old dm (#5C5C5C, ~2.6:1) failed AA and is retired for text.
Small orange text: o400 on dbg is ~4.54:1 — it passes AA but with almost no margin. For orange text below eyebrow size, or anywhere headroom matters, step up to o200 (#EC7D42). Reserve raw o400 text for eyebrows and short accents.
### 2.7 Semantic status colors (functional layer)
Status colors are functional, not brand. They follow a 3-tier model — primitive → semantic → component — so components reference the semantic token (success, error…), never a raw hex. Hues are muted to sit in the industrial palette. Tinted surfaces and borders are derived with Tailwind opacity modifiers (bg-success/10, border-warning/30) — the same recipe as pills (§6.5).
| Semantic token | Hex | Primitive | Meaning |
|---|---|---|---|
| success | #3A7A65 | e400 (eucalyptus) | Done, confirmed, positive, healthy |
| error | #F43F5E | — | Failed, destructive, invalid (validation UX) |
| warning | #D9A441 | amber | Caution, non-critical, needs a look |
| info | #3E6F9E | steel blue | Neutral information |
| progress | #3E6F9E | steel blue (= info) | In progress / active workflow |
| review | #C8923A | ochre | Pending review / needs attention (maps to the forms triage new → review → spam states) |
Rules (hard):
• Never color alone. Every status pairs with an icon and a text label — color is reinforcement, not the only signal (color-blind + grayscale safe).
• Contrast. Status text/icons clear ≥4.5:1 on their surface (≥3:1 large/UI). For small text, put the base token on a tinted (/10) surface rather than base-on-dbg.
• Status ≠ brand. Don't promote a status hue into a brand accent, and never use orange to signal status — orange is brand-only.
• Tokens live in src/app/globals.css @theme; reference by name (text-success, bg-warning/10, border-review/30), never by hex.
> Deprecated brand-green idioms: btn-green, eyebrow-green, and the green pill tone were secondary-accent devices. They're retired as brand variants — green is success only now. Component migration off them is tracked separately (this PR finalizes the system + tokens; it does not sweep every call site).
5. Shape language
• Corners are sharp. Always. border-radius: 0 is the rule. No rounded-* utilities except for .rounded-full on avatar dots, status dots, and the brand loader dot.
• Borders are 1px. Color db (dark) or lb (light). Accented borders (o400, e400) only on active/hover states.
• Default border-style is dotted. All new lines, dividers, card/box borders, and input borders use border-style: dotted. Named permanent exception (always solid): .btn-ghost (always 1px solid).
• Stripe-style max-width frame. The page is framed by:
• Solid, full-bleed horizontal rules (viewport-wide via width: 100vw pseudo-elements from a centered max-width container). Used on .site-footer top border, .site-footer__cta-band::after, .site-footer__small-print::before.
• Dotted vertical max-width rails are section-scoped via the section-rails utility (not a page-wide overlay). Apply the class to any section that has position: relative. Pseudo-elements at max(1.5rem, calc(50% - 616px)) draw the rails within that section only, so they never cross section boundaries or cut horizontal rules. Currently applied on the home hero + the footer's own column dividers.
• Dotted vertical column dividers inside the footer container (.site-footer__col left/right borders) align with the section-rails x-positions so rails flow visually from one section to the next.
• No drop shadows for decoration. Shadows exist only for the primary button hover glow (thermal halo) and the floating score screen in the 404 minigame.
6. Component library
### 6.1 Buttons
```html
Contact us
Watch demo
Filter
Contact us
```
• Primary: bg-o400 text-white, mono 12px/600 uppercase, sheen sweep + 32px thermal glow on hover, 1px lift. White (not o50) is mandatory for WCAG AA - o50 on o400 is 4.08:1 and fails small-text contrast. Semibold (600) plus 12px keeps the label legible at the mono scale.
• Ghost (dark): transparent with border-db, mono 12px/500 uppercase, color dbd, border brightens to dh on hover, 1px lift. Used on dark/charcoal sections.
• Ghost (light): .btn-ghost-light - transparent with dotted border-lb, mono 12px/500 uppercase, color lp, border brightens to lp on hover, 1px lift. Mandatory wherever a ghost button sits in a light-inverted (bg-lbg) section - the dark .btn-ghost tokens become invisible on light bg.
• Green: bg-e400 text-white, mono 12px/600 uppercase, only used inside light/eucalyptus sections. Same rationale as primary: pure white for contrast, semibold for legibility.
• Small variant: add !px-4 !py-2 !text-[11px].
• Disabled: opacity: 0.4; cursor: not-allowed;, never greyed out with a different color.
### 6.2 Eyebrows
Every section header opens with an eyebrow. Dot + mono label.
```html
Every mission type
Platform
```
### 6.3 Section header
Standard anatomy (no section numbers - see Hard rule):
```
[eyebrow] accent square + mono label
[section-title, font-display, italic-accent allowed]
[optional description, text-dbd, max-w-3xl]
```
A reusable `` handles this pattern.
Opener signature (replaces the old giant numerals + corner marks). To keep a distinctive "instrument" cue, the eyebrow's sq accent square carries the identity, and a section opener may lead with a short 1px accent rule (24px wide, o400 on dark / e400 on green sections) directly above the eyebrow. Optional per section, but it is the sanctioned way to add structural emphasis - never reintroduce a large numeral or corner marks.
### 6.4 Cards
A card is one of four variants. Pick by density and role on the page:
| Variant | When to use | Border + fill | Hover idiom |
|---|---|---|---|
| A. Anchor | Hero panels, closing CTAs, large feature visuals (≤ 3 per page) | Filled (bg-ds/bg-ls) + full 1px dotted db/lb border | 1px lift + sheen |
| B. Hairline grid | Repeated grids of 4–8 (benefits, integrations, summary tiles) | No fill, no cell border; outer/internal 1px dotted db/lb divides cells | Gray → o400 (§6.4.1) |
| C. Embedded-title grid | When the section eyebrow + headline can fold into the same grid as its content | No fill; outer 1px dotted db/lb; .bg-dots on the title cell only | Gray → o400 |
| D. Row-list | Editorial indexes — short titles, one-sentence bodies, icon-left | No fill; 1px dotted db/lb between rows | Gray → o400 |
> Hard rule: the anchor card is the only filled, fully-bordered card — reserve it for ≤ 3 hero / CTA / feature moments per page. Repeated grids of 4–8 use the border-only variants (B / C / D). Filling and boxing every cell crosses into "stacked suitcases" — it reads as decoration, not signal.
#### 6.4.A Anchor card (canonical)
The only filled, fully-bordered card. Its identity comes from the bg-ds fill + complete 1px dotted border + the hover lift/sheen — that is what sets it apart from the border-only hairline cells.
```html
```
Icon-headed anchor variant (differentiator / pillar cards): lead with a single Phosphor icon at the top.
```html
Title
Body
```
Icon spec is fixed: size=48, weight="thin", color text-o400, decorative (aria-hidden). One icon per card key — never mix icon and image. See §9 for the icon library rule.
#### 6.4.B Hairline-grid cell
```html
…
…
```
• Outer ` carries border-t border-dotted border-db; cells carry border-b border-r border-dotted border-db`.
• Right-borders are killed off the last cell at each breakpoint via [&:nth-child(2n)]:border-r-0 (substitute 2n, 3n, 4n to match the column count). Tailwind v4 scans markdown for class candidates, so always use a real selector value here — abstract placeholders compile to invalid CSS and break the build.
• Icon defaults to text-dsc; title defaults to text-dh. Both flip to text-o400 on group-hover.
• No bg-ds fill, no ``.
#### 6.4.C Embedded-title grid
The first cell of the grid IS the section header. Marked with .bg-dots for texture; content cells share the same dotted hairline frame.
```html
…
…
…
```
Use when:
• The grid has 4 items (perfect 2×2 against a tall title cell), and
• The section eyebrow + headline can stand alone in the cell width.
A small + glyph top-right of each content cell (6×6 dotted square, text-dsc → text-o400 on hover) is optional but signals "more" / interactive intent without making the cell a button.
#### 6.4.D Row-list
Icon LEFT in a small dotted-bordered square, title + body RIGHT, dotted hairlines between rows. 1 or 2 columns wide.
```html
…
…
```
Reads as an editorial index, not a tile grid. Choose this when titles are short (≤ 3 words) and bodies are one sentence.
#### 6.4.1 Hover idiom (all non-anchor variants)
Mirrors .partner-card. Always transition-colors (200ms default). Never animate the body — keep it readable through hover.
| Element | Default | Hover (group-hover) |
|---|---|---|
| Icon | text-dsc | text-o400 |
| Icon frame border (row-list, framed icon) | border-db | border-o400 |
| Title | text-dh | text-o400 |
| Body | text-dbd opacity-85 | unchanged |
| + indicator | text-dsc border-db | text-o400 border-o400 |
#### 6.4.2 Light-mode card token swap
Light-base (bg-lbg) sections invert the surface and structural tokens. The accent and hover idiom stay identical — only the neutral tokens flip.
| Token role | Dark default | Light swap |
|---|---|---|
| Surface | bg-dbg | bg-lbg |
| Card / icon-frame fill | bg-ds | bg-ls |
| Border / hairline | border-db | border-lb |
| Heading | text-dh | text-lp |
| Body | text-dbd opacity-85 | text-lsc |
| Muted icon (default) | text-dsc | text-lsc |
| Hover accent | text-o400 | text-o400 (unchanged — .partner-card precedent) |
#### 6.4.3 Mixing variants on a page
A page with three or more grids must not use the same §6.4 variant in consecutive sections. Pair them so the eye gets visual rests:
> Anchor (hero) → Hairline grid → Anchor (alternating block) → Embedded-title grid → Row-list → Anchor (closing CTA)
Same-variant repetition is the leading cause of the "boxes-on-boxes-on-boxes" critique. Light-mode inversions (§6.4.2) count as additional rhythm levers — at most two per page, never adjacent.
### 6.5 Pills and status tags
Tones: orange (o400 10% / o200 / o400 30%, brand) and neutral (ds / dbd / db). Status tags use the §2.7 semantic tokens with the same recipe — base on a /10 surface, /30 border, brighter text on dark: success, error, warning, info / progress, review. (The old green tone is now success.)
### 6.6 Inputs and forms
• Transparent background, single bottom border (border-db), border turns o400 on focus.
• Checkbox: 16px square, border db, fills with o400 + white tick when checked.
• Toggle: 40×20 pill, track de off / o400 on, thumb dh, 300ms slide.
• Field labels are .meta-label above the input.
### 6.7 Links
Underline scales from right-to-left on hover over 500ms cubic-bezier(0.22,1,0.36,1).
### 6.8 Navigation
Fixed top nav, 56px tall. Transparent over hero; after 12px scroll, bg-dbg/85 backdrop-blur-xl + border-b border-db. Links are mono 11px uppercase, text-dbd default, text-o400 on active route.
### 6.9 Footer
• Big wordmark band (Lora, 48-72px) with brand dot, then a 4-5 column link grid with mono section labels.
• Newsletter capture uses the square input + bg-o400 submit button pattern.
• Bottom strip: © copy on the left, mono compliance badges (SOC 2, ISO 27001, GDPR, HIPAA) on the right.
### 6.10 Tooltips and popovers
Solid brand surface (bg-ds border border-db), never glassmorph.
• 320px wide, 20px padding.
• Opens on group-hover / group-focus-within, fades 300ms with a 2→0px translate.
• Downward arrow is a 12×12 rotated square tailing from the bottom.
• Dashed divider between body and footer blocks.
### 6.11 Welcome loader
Mounts on the home route via portal to document.body. Black screen (bg-dbg) with an orange dot and a cycling Lora-italic word in 10 languages (matching our locale set). Expo easing in and out, autoAlpha fade on complete. Keyed by route so every navigation to / replays it.
### 6.12 Trust bar (logo strip)
Centered horizontal logo strip, used above-the-fold to signal customer credibility. Implementation: IndustryTrustBar.tsx.
```html
Trusted by …
```
• Caption uses .meta-label (Geist Mono, 10px, uppercase, 0.12em).
• Layout is flex flex-wrap justify-center — never a fixed column grid. Logo counts vary by industry; centering must hold for any count.
• Logos cap at 32-36px tall, monochrome, opacity-80. Source SVGs only.
### 6.13 Industry hero band
Top-of-page hero on every industries/[slug] route. Implementation: IndustryHero.tsx.
```
[eyebrow with sq bg-o400] OIL & GAS
[hero-title] Lora display, ≤2 lines
[body] text-dbd, max-w-xl
[btn-primary + ArrowRight]
```
• Background: full-bleed industry photo with two stacked dbg gradients (left → transparent, top → bottom) for legibility.
• Layout: 12-col grid; text occupies md:col-span-7 lg:col-span-6.
• Eyebrow uses §6.2 pattern with ` and the industry name from ${namespace}.name`.
• Padding: pt-28 pb-32 mobile, md:pt-40 md:pb-48.
### 6.14 Auto-cycling tab strip (transformation band)
Segmented tabs with a sliding orange progress bar. Used for outcome / transformation bands where multiple parallel narratives share a media slot. Implementation: IndustryTransformationTabs.tsx.
Anatomy:
```
[2-col header] H2 section-title left · optional body right (max-w-md)
[strip] grid grid-cols-2 md:grid-cols-4, border-y border-db
└ cell: meta-label number (o400) + Lora label, px-5 py-5
└ active: bg-ds text-dh, 2px progress bar at bottom
└ inactive: text-dsc, hover:text-dh
[content row] grid md:grid-cols-2 — text + image (image in a §6.4.A anchor frame)
```
• Strip uses equal-width grid cells with a vertical hairline (border-r border-db) between them. On mobile (grid-cols-2) cells also carry a bottom hairline.
• Progress bar is a single absolute-positioned 2px bg-o400 span inside the active cell, inset-x-0 bottom-0, transform-origin: left. Animated by the tab-progress keyframe (see §8.8).
• The bar is remounted via key={activeIndex} on tab change so the animation replays from scaleX(0) under the new cell.
• Active background bg-ds cross-fades via transition-colors duration-300.
• Auto-advance cadence: 5500ms. Pauses on mouseenter / focus of the strip; resumes on leave.
• Disabled (no auto-advance) when prefers-reduced-motion: reduce is set; user can still click.
8. Motion
> Motion confirms cause and effect. It shouldn't dance, it should signal.
### 8.1 Stack
• GSAP 3.15+ for scripted timelines (hero, stagger, page transitions, count-ups, 404 minigame).
• CSS transitions for hover-driven UI primitives.
• IntersectionObserver drives scroll reveals and scroll-driven active states.
• Plugins available free (GSAP 3.13+): Draggable, InertiaPlugin, Physics2DPlugin.
### 8.2 Durations
| Name | ms | Use |
|---|---|---|
| Fast | 200 | Hover state change |
| Standard | 400 | Most transitions |
| Reveal | 900 | On-scroll entrance |
| Hero | 1200 | Page load, hero word-stagger |
### 8.3 Easings
• power2.out: default UI
• power4.out: big hero reveal
• expo.out / expo.inOut: dramatic entries (welcome loader)
• elastic.out(1, 0.5): playful, rare
• cubic-bezier(0.22, 1, 0.36, 1): signature CSS curve (button sheen, underline)
### 8.4 Principles
1. Stagger by 55-60ms for list reveals.
2. Translate, don't just fade. Elements rise 16-24px into place. A pure opacity fade reads as loading, not as intention.
3. Out beats In. Easing out feels arrived. Easing in feels rushed. Default to power2.out; in-curves only for exits.
4. Every hover moves 1px up. Buttons, chips, cards.
5. Respect `prefers-reduced-motion`. Every motion block checks the media query and falls back to a static state.
### 8.5 Reveal pattern
Any element with class reveal starts at opacity: 0; transform: translateY(24px) and is animated to opacity: 1; y: 0 once it crosses threshold: 0.12 with rootMargin: 0px 0px -8% 0px. data-delay="0.05" attributes stagger them.
Implementation is a Client Component that runs an IntersectionObserver, triggers GSAP animations on entry, and unobserves. See src/components/motion/Reveal.tsx once implemented.
### 8.6 Scroll-driven active states
For sticky side-panel layouts, use IntersectionObserver with rootMargin: '-45% 0px -45% 0px' to pin the active index to whatever card is crossing the viewport center.
### 8.7 Page transitions
Route change: current page fades out + drops 16px over 400ms; new page enters the inverse way. Wired through the App Router, respects reduced-motion.
### 8.8 Auto-cycle progress bar (tab-progress)
Used by the §6.14 auto-cycling tab strip. A 2px orange bar fills its cell from left to right over the cycle interval, then is remounted under the next active cell and replays.
```css
@keyframes tab-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
```
• Easing is linear so the fill rate matches the timer one-to-one.
• Duration must equal the JS auto-advance interval (currently 5500ms).
• The animated span sets transform-origin: left and animation-play-state: paused while the strip is hovered/focused, so the bar freezes mid-fill rather than restarting.
• Skipped under prefers-reduced-motion: reduce: the underlying auto-advance is also disabled, so the bar simply doesn't animate.
10. Content and copy
• Specific numbers always beat adjectives. "3.4k autonomous flights/yr" beats "many flights".
• Eyebrows are 2-4 words, uppercase. They answer "what am I looking at?" before the headline.
• Headlines earn one italic. Accent a single noun, not a verb.
• Body copy caps at ~65ch for long blocks, ~40ch for hero text.
• Voice is operator-to-operator. Avoid marketing superlatives. Prefer facts, timestamps, telemetry.
11. Accessibility
• Minimum contrast 4.5:1 for body/label text, 3:1 for large text. Every dark-base text token meets this — see the §2.6 hierarchy (dbd ~12:1, dsc ~6.1:1, dm ~5:1). Tokens darker than dm (db, de) are borders/decoration only, never text.
• Every interactive element shows a visible :focus-visible state — a 2px `o400` outline with 2px offset — applied consistently across links, buttons, inputs, and custom controls (not only custom ones). Keyboard focus must never be suppressed without an equivalent replacement.
• prefers-reduced-motion: reduce disables every non-essential animation (hero word-stagger, welcome loader).
• Tooltips and popovers must also open on :focus-within, not only :hover.
• Alt text on every meaningful image; empty alt for decorative only.
• Form inputs always have an associated label or aria-label.
• Semantic HTML first: `, , , , , `.
12. Tech baseline
This design system runs on FlytBase-26's committed stack:
• Next.js 16 (App Router, RSC)
• React 19
• TypeScript 5 strict
• Tailwind CSS v4 (@theme CSS directives, not tailwind.config.js)
• Next/font for Lora and Geist
• GSAP 3.15+ for scripted motion (Client Components only)
• IntersectionObserver API for scroll reveals
Reference implementation (design system as built): /Users/flytbaselabs/Desktop/Website Hackthon/Flat Creative/flytbase-rebrand/. When porting component primitives, reference that repo's DesignSystem.jsx page for the canonical rendering.
13. Token reference (Tailwind v4)
Tailwind v4 uses CSS-first theme declarations. Paste this into src/app/globals.css at the top, after font imports:
```css
@theme {
/ Charcoal (dark base) /
--color-dbg: #1A1A1A;
--color-ds: #242424;
--color-de: #2E2E2E;
--color-db: #3D3D3D;
--color-dm: #8A8A8A;
--color-dsc: #999999;
--color-dbd: #D6D6D6;
--color-dh: #F0F0F0;
/ Cool gray (light base) /
--color-lbg: #F5F5F7;
--color-ls: #FFFFFF;
--color-lb: #DDDFE3;
--color-lsc: #555555;
--color-lp: #111111;
/ Signal orange /
--color-o50: #FDF0E8;
--color-o100: #F4B896;
--color-o200: #EC7D42;
--color-o400: #D95B28;
--color-o600: #A33D14;
--color-o800: #7A2B0E;
--color-o900: #4A1808;
/ Eucalyptus /
--color-e50: #EAF0EE;
--color-e100: #BACDD0;
--color-e200: #72A899;
--color-e400: #3A7A65;
--color-e600: #2B5A4A;
--color-e800: #1B3A30;
--color-e900: #0F2219;
/ Validation pink (never as brand color) /
--color-error: #F43F5E;
/ Fonts: wired via next/font variables /
--font-display: var(--font-lora);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
/ Tracking /
--tracking-tightest: -0.04em;
--tracking-tighter2: -0.02em;
--tracking-wideish: 0.06em;
--tracking-wider2: 0.08em;
--tracking-widest2: 0.12em;
/ Container /
--container-page: 1200px;
/ Thermal gradient /
--background-image-thermal: linear-gradient(135deg, #FE5C0C 0%, #A90901 35%, #5C1A0A 55%, #2E1510 70%, #1A1A1A 100%);
}
```
Companion @layer components block (same file, after @theme):
```css
@layer components {
.eyebrow {
@apply inline-flex items-center gap-2 font-mono uppercase;
font-size: 10px;
letter-spacing: 0.12em;
color: var(--color-o400);
}
.eyebrow-green {
@apply eyebrow;
color: var(--color-e400);
}
.sq {
@apply inline-block;
width: 8px;
height: 8px;
}
.section-title {
@apply font-display;
font-size: clamp(32px, 4vw, 56px);
line-height: 1.1;
letter-spacing: -0.02em;
}
.meta-label {
@apply font-mono uppercase;
font-size: 10px;
letter-spacing: 0.08em;
color: var(--color-dm);
}
/ Buttons /
.btn-primary {
@apply inline-flex items-center gap-2 font-mono uppercase transition-all;
font-size: 11px;
letter-spacing: 0.08em;
padding: 14px 24px;
background: var(--color-o400);
color: var(--color-o50);
border: 1px solid var(--color-o400);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 0 32px rgba(217, 91, 40, 0.4);
}
.btn-ghost {
@apply inline-flex items-center gap-2 font-mono uppercase transition-all;
font-size: 11px;
letter-spacing: 0.08em;
padding: 14px 24px;
background: transparent;
color: var(--color-dbd);
border: 1px solid var(--color-db);
}
.btn-ghost:hover {
transform: translateY(-1px);
border-color: var(--color-dh);
color: var(--color-dh);
}
.btn-green {
@apply btn-primary;
background: var(--color-e400);
border-color: var(--color-e400);
color: white;
}
/ Patterns /
.bg-grid {
background-image:
linear-gradient(rgba(255,255,255,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.04) 1px, transparent 1px);
background-size: 32px 32px;
}
.bg-grid-light {
background-image:
linear-gradient(rgba(0,0,0,0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.06) 1px, transparent 1px);
background-size: 32px 32px;
}
.bg-dots {
background-image: radial-gradient(rgba(255,255,255,0.14) 1px, transparent 1px);
background-size: 20px 20px;
}
/ Reveal baseline /
.reveal {
opacity: 0;
transform: translateY(24px);
}
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
}
}
}
```
Full component CSS lives in src/app/globals.css and is the single source of truth. Any UI primitive added later (toggle, checkbox, link-underline) adds to this block.
14. Hard rules (never break)
These are non-negotiable. If a design deviates from any of them, it is wrong.
1. One brand accent (`o400`) only. Never introduce a second brand accent, and never use orange to signal status — status colors (success/error/warning/info/progress/review, §2.7) are functional and may appear in any section.
2. Sharp corners only. border-radius: 0, except rounded-full on dots and avatars.
3. Eyebrows are Geist Mono, 10px, uppercase, 0.12em letter-spacing, and start with a 8×8px accent square.
4. Headlines are Lora. Interface copy is Geist. Metadata is Geist Mono. Do not swap.
5. 8px grid. Every spacing value is a multiple of 8.
6. Max content width is 1200px.
7. Italic only accents one word in a headline. Never a whole sentence.
8. Hover moves 1px up, not down. Buttons, cards, chips.
9. No glassmorph. Popovers are solid brand surfaces.
10. No emoji in UI or marketing copy.
11. Respect `prefers-reduced-motion`. Every animation has a static fallback.
12. No rounded buttons, no gradient buttons (except primary sheen), no shadow-only borders.
13. Dark base is the default. Light sections are deliberate inversions, not themes.
14. Every section gets one accent, one eyebrow, one headline, and a max-w description, in that order.
15. Error pink (`#F43F5E`) is for validation UX only. Never as a brand color.
16. Phosphor Thin weight for icons. Regular weight only inside bordered 32-40px squares. Never duotone or fill.
17. No design system drift. Every UI element resolves to a DESIGN-SYSTEM.md primitive. External references (images, code, URLs, descriptions) get parsed for structural intent, never copied verbatim. New primitives require explicit approval and become part of DESIGN-SYSTEM.md. Silent inline variants are forbidden. See Docs/AGENTS.md "No Design System Drift" for the protocol.
18. Every popup/overlay dims and blurs the page behind it. Modals, dropdown panels, locale switchers, command palettes, and any other layer that sits above the normal document MUST render a dedicated backdrop element (covering the full viewport) with background: rgba(0, 0, 0, 0.6) plus a backdrop-filter: blur(10px) pair. Reference: .mega-nav__backdrop, .locale-switcher__backdrop. Always pass the blur value through a local CSS custom property, e.g. --popup-backdrop-blur: blur(10px); -webkit-backdrop-filter: var(--popup-backdrop-blur); backdrop-filter: var(--popup-backdrop-blur);. Writing the prefixed/unprefixed declarations as plain literals causes Tailwind v4's lightningcss optimizer to drop the unprefixed form, which silently disables the blur in Chrome and Firefox. Click-on-backdrop closes the overlay.
19. Mix card variants within a page. Two consecutive grid sections must not use the same §6.4 variant. Same-variant repetition produces the "stacked suitcases" effect; alternating with hairline-grid, embedded-title, row-list, and light-mode inversions (§6.4.2) gives the eye the visual rests it needs.
20. Text meets contrast. Body, label, and secondary text clear ≥4.5:1 on their background (large text ≥3:1). Never use a text color darker than dm (#8A8A8A) on dbg; darker neutrals (db, de) are for borders/decoration only. See §2.6.
21. No section numbers, no corner marks. Section openers are eyebrow → headline → description — never a giant numeral. Cards are never decorated with L-shaped corner marks; the anchor card earns its emphasis through fill + a full dotted border (§6.4.A). Both were retired as decoration-not-signal.
Navigation & Footer
How the primary site navigation and footer are built. These are the two most prominent shared components — they sit outside any page route and use design system primitives without inventing new ones.
### Files
• src/components/nav/MegaNav.tsx — client component, renders the bar, dropdowns, and mobile menu. All user-facing strings come from nav.megaNav in src/messages/en.json.
• src/components/nav/mega-nav-init.ts — scoped GSAP controller. Takes a root element, returns a cleanup. Handles directional hover, panel morphing, keyboard navigation, mobile slide-over, backdrop click, resize.
• src/components/footer/SiteFooter.tsx — full-width footer: top CTA band, 5-column sitemap, small print. Shares link-hover and active-state styling with the nav.
• src/app/globals.css — all nav + footer styling (.mega-nav__*, .site-footer__*, hover-underline, active-state).
• [src/app/[locale]/layout.tsx](../src/app/[locale]/layout.tsx) — mounts ` and inside the NextIntlClientProvider`.
### Brand rules that apply
• Brand string is always "FlytBase" — use t('brand') from the footer or nav.megaNav namespace.
• Icons are @phosphor-icons/react at weight="thin" only. No emoji, no custom SVG.
• No em or en dashes anywhere in translations.
• All link primitives come from @/i18n/navigation (Link, usePathname). Never from next/link.
• English URLs have no locale prefix. Do not hardcode /en/... anywhere.
• Buttons use the design-system classes: .btn-primary, .btn-ghost, .btn-green, optionally with .btn-sm. Do not define component-specific CTA styles.
### Active-state and hover-underline
Both the nav and the footer share one interaction contract:
• Every nav or footer link renders a 1px underline that animates from scaleX(0) to scaleX(1) on hover using cubic-bezier(0.22, 1, 0.36, 1).
• When a link's href matches the current pathname (from usePathname()), the component sets data-active="true" on the element. CSS then paints the text and underline in --color-o400 (Signal Orange) and holds the underline at full width.
• Dropdown parents in the nav (Platform, Industries, Partners, Resources) mark themselves active when any of their panel children match the current route. The Resources card CTA points to /case-studies. The Brand guidelines link lives in the footer's Resources column, not in the nav.
### MegaNav structure
```
...
...
Log in
Get started
...
...
...
```
#### Data attributes the controller listens for
| Attribute | Where | Used for |
| --- | --- | --- |
| data-menu-wrap | `` | Root handle for the controller. |
| data-menu-open | ` | "true"` while any panel or the mobile menu is open. Drives the backdrop. |
| data-nav-list / data-mobile-nav | Inner container | Target for mobile fade + slide. |
| data-nav-list-item | Each `` | Staggered in on mobile open. |
| data-dropdown-toggle="" | Panel trigger buttons | Controls hover-intent open. |
| data-dropdown-wrapper / data-dropdown-container / data-dropdown-bg | Dropdown scaffolding | Height morphing between panels. |
| data-nav-content="" / data-panel-state | Panel regions | Directional fade-in content. |
| data-menu-fade | Any child inside a panel | Opt-in to the panel fade/stagger. |
| data-menu-backdrop | Backdrop overlay | Click-to-close. |
| data-burger-toggle / data-burger-line | Mobile menu button + its three spans | Rotates lines to an X on open. |
| data-mobile-back / data-menu-logo | Mobile back button + logo | Swapped when a mobile panel is active. |
| data-active="true" | Any link or dropdown toggle | Marks the current route for accent styling. |
#### Adding a new top-level item
1. Add the label under nav.megaNav.top. in src/messages/en.json.
2. Add a ` to the mega-navbar-list` in `MegaNav.tsx`. Use `` for a flat link, or `" class="mega-navbar-link is--dropdown">` for a panel.
3. For a panel, add a "> block inside data-dropdown-container, declare your columns as a PanelColumn[], and add the matching translations under nav.megaNav....
4. If the new item can be the active route, extend the active memo in MegaNav.tsx with the ownership rule (flat: exact match; panel: ownsAnyHref() or a manual pathname check for entries that live outside the columns).
#### Mobile slide-over
Below 991px the controller switches to mobile mode: opening the burger fades the bar list in, tapping a panel trigger slides it in and hides the logo in favour of a back button. The controller owns document.body.style.overflow while the mobile menu is open and restores it on close. The resize handler flushes both modes when the viewport crosses the breakpoint.
#### Keyboard and focus
• Enter, Space, or ArrowDown on a trigger opens the panel and focuses the first link inside.
• ArrowUp closes the panel.
• Tabbing out of the last focusable element in a panel closes it.
• Escape closes any open panel (desktop) or the mobile menu.
• Focus is restored to the trigger after close.
#### Reduced motion
prefers-reduced-motion: reduce is honoured by the shared button + reveal rules in globals.css. Panel GSAP timelines still run but without translateY lift; do not disable opacity transitions or the controller's state management breaks.
### SiteFooter structure
```
...
...
Start now
Contact sales
...
...
...
...
...
© {year} FlytBase...
...
```
#### Adding a column item
1. Add the label under footer.columns.. in en.json.
2. Push { key, href } onto the matching Col array in SiteFooter.tsx. The component resolves the label as t('columns..').
3. If the new href points to an internal route, active-state styling turns on automatically via usePathname() matching.
#### Adding a new column
1. Add a label under footer.sections. in en.json.
2. Add the items as footer.columns.. in en.json.
3. Define a Col in SiteFooter.tsx and push it into the columns array.
4. Extend the grid in globals.css if you want more than five columns.
### Verification checklist (nav + footer)
Run before merging any change to the nav or footer:
• npm run type-check
• npm run lint
• Open /, /brand-guidelines, /fr, /ar — confirm the nav bar and footer render, the brand string is "FlytBase", and only Phosphor icons at weight="thin" are visible.
• Hover every nav and footer link — underline should animate in and out, text hue shifts to --color-dh.
• Navigate to /brand-guidelines — Brand guidelines in the Resources footer column should hold the orange accent and a persistent underline.
• Shrink the viewport below 991px — burger opens the mobile menu, panel triggers slide to the panel, back button returns to the root list, document.body.style.overflow is restored on close.
• Confirm prefers-reduced-motion: reduce disables the button lift and grain animation.
No Design System Drift
When ingesting any reference - a screenshot, image, code snippet, competitor URL, Figma file, or verbal description - follow this protocol without exception:
### Step 1: Extract structural intent, not visual style
Ask "what is this element trying to do structurally?", not "how do I replicate this visually?". A rounded blue button with a drop shadow in a reference is structurally a primary CTA. Our primary CTA is .btn-primary (sharp corners, signal orange, thermal halo). The structure maps; the visual treatment does not need to.
### Step 2: Map to DESIGN-SYSTEM.md primitives
Every UI element must resolve to an existing primitive documented in Docs/DESIGN-SYSTEM.md:
| Reference contains | Use |
|---|---|
| Any button (primary action) | .btn-primary |
| Any button (secondary/outline) | .btn-ghost |
| Any button in light context | .btn-green |
| Section opener | `` pattern: eyebrow → section-title → description (no section number) |
| Card / feature block (anchor: hero, closing CTA, large feature visual) | §6.4.A — filled bg-ds/bg-ls card with a full 1px dotted border + hover lift (no corner marks) |
| Card / feature block (repeating grid of 4–8) | §6.4.B — hairline-grid cell, no border, dotted internal dividers, gray→o400 hover |
| Card / feature block (grid where the section header folds into the grid) | §6.4.C — embedded-title grid with .bg-dots title cell |
| Card / feature block (editorial index, icon-left + content-right) | §6.4.D — row-list with hairline dividers between rows |
| Tag / chip | Pill pattern (orange/green/neutral tone, from DESIGN-SYSTEM.md §6.5) |
| Form input | .input-field |
| Icon | @phosphor-icons/react Thin weight (Regular weight only in bordered 32-40px squares) |
| Divider | border-b border-db (dark) or border-b border-lb (light) |
| Any heading | .section-title (Lora) |
| Any metadata / label | .meta-label or .eyebrow (Geist Mono) |
| Any color | From the token set: o50-o900, e50-e900, dbg/ds/de/db/dm/dsc/dbd/dh, lbg/ls/lb/lsc/lp |
| Any spacing | Multiple of 8 (DESIGN-SYSTEM.md §4.1) |
### Step 3: If no primitive exists, STOP and ask
Do not invent a new variant inline. Do not silently add a new button style, font weight, color, spacing value, shadow, or corner radius. Instead, post a proposal:
> "The reference shows [X]. No existing primitive in DESIGN-SYSTEM.md covers this pattern. Two options:
> 1. Add a new primitive to DESIGN-SYSTEM.md (becomes part of the system, reusable). Specify the token, usage, hard rules.
> 2. Skip this element / substitute an existing primitive / flag it as deferred.
> Which should I do?"
Wait for an explicit answer before writing code.
### Step 4: Detect and flag off-brand references
When you see any of these in a reference, state the mismatch explicitly before proceeding:
• Rounded corners (other than rounded-full on dots/avatars)
• Drop shadows used decoratively
• Gradient fills (except the named thermal gradient on hero glow)
• Glassmorph / backdrop-blur used for decoration, not the nav
• Font families other than Lora, Geist, Geist Mono
• Colors outside the DESIGN-SYSTEM.md palette (no Tailwind gray-*, blue-*, etc.)
• Hover effects that move down instead of up
• Border radius values other than 0 or full
• Text weight beyond 400/500/600/700 ranges in DESIGN-SYSTEM.md §3.2
• Custom SVG icons (exception: documented one-offs in src/components/icons/ per Q8)
• Emoji in UI
Example response when ingesting a reference: "Reference shows rounded-lg buttons with shadow-md. DESIGN-SYSTEM.md §5 requires sharp corners and prohibits decorative shadows. I'll use .btn-primary with sharp corners and the thermal halo on hover instead. Confirm?"
### Step 5: Never silently deviate
If the user pushes back ("no, I want the rounded version this time"), do not just comply. Respond:
> "Accepting this deviates from DESIGN-SYSTEM.md §5. Two paths:
> 1. Update DESIGN-SYSTEM.md §5 to allow this (system change, propagates everywhere).
> 2. Hard-code as a one-off, document in Docs/OPEN-QUESTIONS.md as tech debt.
> Which?"
One-offs must be explicitly acknowledged as tech debt. Silent deviation is forbidden.
### Applies to
• Image references (screenshots, mockups, Figma exports, competitor sites)
• Code snippets from other projects, libraries, or documentation
• Verbal descriptions ("I want it to look like Stripe's pricing page")
• URL references ("make it similar to linear.app/homepage")
• AI-generated design outputs (v0.dev, Galileo, etc.)
The source doesn't matter. The rule is always: extract intent, map to primitives, ask before inventing, flag deviations.
Use with MCP
Don't have the MCP? Install it here