diff --git a/docs/5n-infra/index.md b/docs/5n-infra/index.md new file mode 100644 index 0000000..8abacce --- /dev/null +++ b/docs/5n-infra/index.md @@ -0,0 +1,8 @@ +# 5N Infra + + +5N provides hosted node service where we can operate operator for you. Contact nodesupport@fivenorth.io for detail. + +## Canton Network + +5N is a Supper Validator on Canton network and we can also help with your onboarding to Canton by sponsoring your node. Contact nodesupport@fivenorth.io for detail \ No newline at end of file diff --git a/docs/assets/fn/footer-logo.svg b/docs/assets/fn/footer-logo.svg new file mode 100644 index 0000000..301ec21 --- /dev/null +++ b/docs/assets/fn/footer-logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/fn/icon-5n-dashboard.svg b/docs/assets/fn/icon-5n-dashboard.svg new file mode 100644 index 0000000..850be55 --- /dev/null +++ b/docs/assets/fn/icon-5n-dashboard.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/fn/icon-5n-infra.png b/docs/assets/fn/icon-5n-infra.png new file mode 100644 index 0000000..e980703 Binary files /dev/null and b/docs/assets/fn/icon-5n-infra.png differ diff --git a/docs/assets/fn/icon-5n-time-machine.svg b/docs/assets/fn/icon-5n-time-machine.svg new file mode 100644 index 0000000..926ce6e --- /dev/null +++ b/docs/assets/fn/icon-5n-time-machine.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/assets/fn/icon-arrow.svg b/docs/assets/fn/icon-arrow.svg new file mode 100644 index 0000000..6504ed2 --- /dev/null +++ b/docs/assets/fn/icon-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/fn/icon-coming-soon.svg b/docs/assets/fn/icon-coming-soon.svg new file mode 100644 index 0000000..c219901 --- /dev/null +++ b/docs/assets/fn/icon-coming-soon.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/fn/icon-id-sdk.svg b/docs/assets/fn/icon-id-sdk.svg new file mode 100644 index 0000000..ff499a4 --- /dev/null +++ b/docs/assets/fn/icon-id-sdk.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/assets/fn/icon-loop-sdk.svg b/docs/assets/fn/icon-loop-sdk.svg new file mode 100644 index 0000000..94d5d4e --- /dev/null +++ b/docs/assets/fn/icon-loop-sdk.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/assets/fn/icon-search.svg b/docs/assets/fn/icon-search.svg new file mode 100644 index 0000000..3807844 --- /dev/null +++ b/docs/assets/fn/icon-search.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/fn/logo.svg b/docs/assets/fn/logo.svg new file mode 100644 index 0000000..960a29c --- /dev/null +++ b/docs/assets/fn/logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/social.png b/docs/assets/social.png new file mode 100644 index 0000000..41d70c3 Binary files /dev/null and b/docs/assets/social.png differ diff --git a/docs/index.md b/docs/index.md index 8abacce..fc393a2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,9 @@ -# 5N Infra - - -5N provides hosted node service where we can operate operator for you. Contact nodesupport@fivenorth.io for detail. - -## Canton Network - -5N is a Supper Validator on Canton network and we can also help with your onboarding to Canton by sponsoring your node. Contact nodesupport@fivenorth.io for detail \ No newline at end of file +--- +template: home.html +title: Five North — Foundational Infrastructure for Decentralized Financial Networks +hide: + - navigation + - toc + - path + - footer +--- diff --git a/docs/javascripts/fn-theme.js b/docs/javascripts/fn-theme.js new file mode 100644 index 0000000..d6efd48 --- /dev/null +++ b/docs/javascripts/fn-theme.js @@ -0,0 +1,582 @@ +/* ═══════════════════════════════════════════════════════════════ + FIVENORTH CUSTOM BEHAVIOR + - Theme toggle (Light ⇄ Dark) — default = dark + - Mobile hamburger menu + - Active tab highlight based on URL + - Hero grid animation (Home page only) + + Designed to survive MkDocs Material's `navigation.instant`, which + swaps the content on each navigation. We achieve this by + binding a SINGLE click listener to `document` (which is never + replaced) and dispatching via `closest()`. + ═══════════════════════════════════════════════════════════════ */ + +(function () { + 'use strict'; + + const FN_THEME_KEY = 'fn-theme'; + const SCHEME_LIGHT = 'default'; + const SCHEME_DARK = 'slate'; + + /* ═════════════════ Theme ═════════════════ */ + + function getStoredTheme() { + try { + const v = localStorage.getItem(FN_THEME_KEY); + return (v === 'light' || v === 'dark' || v === 'system') ? v : null; + } catch (_) { return null; } + } + + /* Follow OS preference for first-time visitors. */ + function computeTheme() { + return getStoredTheme() || 'system'; + } + + /* Resolve system preference to actual light/dark */ + function resolveTheme(theme) { + if (theme === 'system') { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + return theme; + } + + function applyTheme(theme) { + const resolved = resolveTheme(theme); + const scheme = resolved === 'light' ? SCHEME_LIGHT : SCHEME_DARK; + document.documentElement.setAttribute('data-md-color-scheme', scheme); + if (document.body) { + document.body.setAttribute('data-md-color-scheme', scheme); + } + } + + function saveTheme(theme) { + try { localStorage.setItem(FN_THEME_KEY, theme); } catch (_) {} + } + + /* Apply theme as early as possible to prevent FOUC. */ + applyTheme(computeTheme()); + + /* Re-apply when OS theme changes (only takes effect when user chose 'system') */ + try { + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function () { + if (computeTheme() === 'system') { + applyTheme('system'); + updatePillActive(); + } + }); + } catch (_) {} + + /* ═════════════════ Mobile menu helpers ═════════════════ */ + + function closeMobileMenu() { + const drawer = document.querySelector('[data-fn-mobile-nav]'); + const btn = document.querySelector('[data-fn-hamburger]'); + const line = btn && btn.querySelector('.fn-hamburger-line'); + if (drawer) drawer.classList.remove('fn-mobile-nav-open'); + if (line) line.classList.remove('fn-hamburger-open'); + if (btn) btn.setAttribute('aria-expanded', 'false'); + } + + function toggleMobileMenu(btn) { + const drawer = document.querySelector('[data-fn-mobile-nav]'); + const line = btn.querySelector('.fn-hamburger-line'); + if (!drawer || !line) return; + const open = drawer.classList.toggle('fn-mobile-nav-open'); + line.classList.toggle('fn-hamburger-open', open); + btn.setAttribute('aria-expanded', String(open)); + } + + /* ═════════════════ Mobile search overlay ═════════════════ + .fn-search-wrap is now a direct sibling of .fn-header__sticky at + body level — no backdrop-filter ancestor — so position:fixed is + always viewport-relative. We just toggle body.fn-mobile-search-active + and CSS turns .fn-search-wrap into a full-screen overlay. */ + + var _mobileSearchOpen = false; + var _searchFocusRetry = null; + + /* Material emits `` at body + level. Its search module is wired to that exact element via + `watchToggle(getElement("#__search"))`, so toggling it is how we tell + Material the search UI is active. */ + function setMdSearchToggle(on) { + var t = document.getElementById('__search'); + if (!t || t.checked === on) return; + t.checked = on; + t.dispatchEvent(new Event('change', { bubbles: true })); + } + + /* The real query field has NO id — it's tagged + `data-md-component="search-query"`. Earlier code used + `getElementById('__search')` which returned the hidden CHECKBOX, so + focus/value/input events went to the wrong element on mobile. */ + function getSearchInput() { + return document.querySelector('.md-search__input') + || document.querySelector('[data-md-component="search-query"]'); + } + + function closeMobileSearch() { + if (!_mobileSearchOpen) return; + _mobileSearchOpen = false; + if (_searchFocusRetry) { clearInterval(_searchFocusRetry); _searchFocusRetry = null; } + document.body.classList.remove('fn-mobile-search-active'); + document.querySelectorAll('[data-fn-search-toggle]').forEach(function (b) { + b.setAttribute('aria-expanded', 'false'); + b.classList.remove('is-active'); + }); + setMdSearchToggle(false); + var q = getSearchInput(); + if (q) { + q.value = ''; + q.blur(); + q.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + function resetMobileSearchState() { + _mobileSearchOpen = false; + document.body.classList.remove('fn-mobile-search-active'); + setMdSearchToggle(false); + document.querySelectorAll('[data-fn-search-toggle]').forEach(function (b) { + b.setAttribute('aria-expanded', 'false'); + b.classList.remove('is-active'); + }); + } + + function openMobileSearch() { + if (_mobileSearchOpen) return; + _mobileSearchOpen = true; + document.body.classList.add('fn-mobile-search-active'); + document.querySelectorAll('[data-fn-search-toggle]').forEach(function (b) { + b.setAttribute('aria-expanded', 'true'); + b.classList.add('is-active'); + }); + + /* Tell Material's search JS that search is active → enables result routing */ + setMdSearchToggle(true); + + /* Focus the real text input — wait for CSS display change to take effect */ + var q = getSearchInput(); + if (q) { + requestAnimationFrame(function () { + q.focus(); + var tries = 0; + if (_searchFocusRetry) { clearInterval(_searchFocusRetry); } + _searchFocusRetry = setInterval(function () { + if (document.activeElement === q || ++tries > 8) { + clearInterval(_searchFocusRetry); + _searchFocusRetry = null; + } else { + q.focus(); + } + }, 50); + /* Dispatch input event so Material processes any pre-existing value + and activates its result pipeline on mobile. */ + q.dispatchEvent(new Event('input', { bubbles: true })); + }); + if (!q.dataset.fnMobileSearchEsc) { + q.dataset.fnMobileSearchEsc = '1'; + q.addEventListener('keydown', function (e) { + if (e.key === 'Escape') closeMobileSearch(); + }); + } + + /* Material's own × (type=reset) clears input but not our overlay. + Intercept the form reset and close our mobile search too. */ + var form = q.closest('form'); + if (form && !form._fnResetBound) { + form._fnResetBound = true; + form.addEventListener('reset', function () { + if (_mobileSearchOpen) { + /* Let the native reset clear input first, then close overlay */ + setTimeout(closeMobileSearch, 0); + } + }); + } + } + + /* Inject a search icon inside the form (once), positioned absolute on the left */ + var wrap = document.querySelector('[data-fn-search-wrap]'); + if (wrap && !wrap.querySelector('.fn-mobile-search-icon')) { + var searchIcon = document.createElement('span'); + searchIcon.className = 'fn-mobile-search-icon'; + searchIcon.setAttribute('aria-hidden', 'true'); + searchIcon.innerHTML = ''; + var form = wrap.querySelector('.md-search__form'); + if (form) form.insertBefore(searchIcon, form.firstChild); + } + + /* Inject a close button into the search wrap on mobile (once) */ + if (wrap && !wrap.querySelector('.fn-mobile-search-close')) { + var closeBtn = document.createElement('button'); + closeBtn.type = 'button'; + closeBtn.className = 'fn-mobile-search-close'; + closeBtn.setAttribute('aria-label', 'Close search'); + closeBtn.innerHTML = ''; + wrap.appendChild(closeBtn); + closeBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + closeMobileSearch(); + }); + } + } + + function bindSearchToggleButtons() { + document.querySelectorAll('[data-fn-search-toggle]').forEach(function (btn) { + if (btn.dataset.fnSearchBound === '1') return; + btn.dataset.fnSearchBound = '1'; + btn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (_mobileSearchOpen) { + closeMobileSearch(); + } else { + closeMobileMenu(); + openMobileSearch(); + } + }); + }); + } + + /* ═════════════════ Active tab highlight ═════════════════ */ + + function isActiveTab(href) { + if (!href) return false; + try { + const url = new URL(href, window.location.origin); + if (url.origin !== window.location.origin) return false; + const path = window.location.pathname.replace(/\/index\.html$/, '/'); + const target = url.pathname.replace(/\/index\.html$/, '/'); + if (target === '/' || target === '') return path === '/' || path === ''; + return path === target || path.startsWith(target); + } catch (_) { return false; } + } + + function updateActiveTabs() { + document.querySelectorAll('[data-fn-tab]').forEach(function (el) { + const href = el.getAttribute('href'); + if (isActiveTab(href)) el.classList.add('is-active'); + else el.classList.remove('is-active'); + }); + } + + /* ═════════════════ Hero grid animation ═════════════════ */ + + const GRID_CELLS = [ + { col: 2, row: 6, color: '#D3D8F3', order: 7, drop: 0 }, + { col: 4, row: 6, color: 'white', drop: 1 }, + { col: 6, row: 6, color: '#E4DCF9', order: 8, drop: 2 }, + { col: 1, row: 5, color: 'white', drop: 3 }, + { col: 3, row: 5, color: 'white', drop: 4 }, + { col: 5, row: 5, color: '#D5F4FE', order: 6, drop: 5 }, + { col: 4, row: 4, color: '#CAEEC8', order: 4, drop: 6 }, + { col: 6, row: 4, color: '#D3D8F3', order: 5, drop: 7 }, + { col: 3, row: 3, color: '#F2FF96', order: 3, drop: 8 }, + { col: 5, row: 3, color: 'white', drop: 9 }, + { col: 7, row: 3, color: 'white', drop: 10 }, + { col: 4, row: 2, color: '#E4DCF9', order: 1, drop: 11 }, + { col: 6, row: 2, color: '#D5F4FE', order: 2, drop: 12 }, + { col: 5, row: 1, color: 'white', drop: 13 }, + { col: 7, row: 1, color: '#CAEEC8', order: 0, drop: 14 }, + ]; + + const PHASE_DURATION = { drop: 3000, blink: 10000, fadeout: 1000, pause: 600 }; + const NEXT_PHASE = { drop: 'blink', blink: 'fadeout', fadeout: 'pause', pause: 'drop' }; + + let heroTimer = null; + + function buildHeroGrid(container) { + container.innerHTML = ''; + GRID_CELLS.forEach(function (cell) { + const el = document.createElement('div'); + const isColored = cell.order != null; + el.className = 'fn-grid-cell' + (isColored ? ' fn-grid-cell-active' : ''); + el.style.gridColumn = String(cell.col); + el.style.gridRow = String(cell.row); + el.style.setProperty('--cell-color', + isColored ? cell.color : 'var(--fn-surface-raised)'); + el.style.setProperty('--drop-i', String(cell.drop)); + if (isColored) el.style.setProperty('--cell-i', String(cell.order)); + container.appendChild(el); + }); + } + + function runHero(container) { + buildHeroGrid(container); + let phase = 'drop'; + container.className = 'fn-hero-grid fn-phase-' + phase; + (function step() { + heroTimer = setTimeout(function () { + phase = NEXT_PHASE[phase]; + container.className = 'fn-hero-grid fn-phase-' + phase; + if (phase === 'drop') buildHeroGrid(container); + step(); + }, PHASE_DURATION[phase]); + })(); + } + + function initHero() { + if (heroTimer) { clearTimeout(heroTimer); heroTimer = null; } + const container = document.querySelector('[data-fn-hero-grid]'); + const containerMobile = document.querySelector('[data-fn-hero-grid-mobile]'); + if (!container && !containerMobile) return; + /* Run a single shared phase loop; both grids mirror each other */ + let phase = 'drop'; + function buildBoth() { + if (container) buildHeroGrid(container); + if (containerMobile) buildHeroGrid(containerMobile); + } + buildBoth(); + if (container) container.className = 'fn-hero-grid fn-phase-' + phase; + if (containerMobile) containerMobile.className = 'fn-hero-grid fn-phase-' + phase; + (function step() { + heroTimer = setTimeout(function () { + phase = NEXT_PHASE[phase]; + if (container) container.className = 'fn-hero-grid fn-phase-' + phase; + if (containerMobile) containerMobile.className = 'fn-hero-grid fn-phase-' + phase; + if (phase === 'drop') buildBoth(); + step(); + }, PHASE_DURATION[phase]); + })(); + } + + /* ═════════════════ Sticky-bar "stuck" detection ═════════════ + The action bar (tabs + search) uses `position: sticky; top: 0`. + To show a subtle divider ONLY when it's actually pinned to the + viewport edge, we watch a 1px sentinel placed just above it. + When the sentinel scrolls out of view → bar is stuck. */ + + let stickyObserver = null; + + function initStickyBar() { + if (stickyObserver) { stickyObserver.disconnect(); stickyObserver = null; } + + const bar = document.querySelector('[data-fn-sticky-bar]'); + if (!bar) return; + + // Ensure a sentinel right above the bar, inside .fn-header. + let sentinel = document.querySelector('[data-fn-sticky-sentinel]'); + if (!sentinel) { + sentinel = document.createElement('div'); + sentinel.setAttribute('data-fn-sticky-sentinel', ''); + sentinel.style.cssText = 'height:1px;width:100%;pointer-events:none;'; + bar.parentNode.insertBefore(sentinel, bar); + } + + stickyObserver = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + // When sentinel is NOT intersecting (scrolled out), bar is stuck. + if (!entry.isIntersecting) bar.classList.add('is-stuck'); + else bar.classList.remove('is-stuck'); + }); + }, { threshold: 0, rootMargin: '0px' }); + + stickyObserver.observe(sentinel); + } + + /* ═════════════════ Theme pill binding ═════════════════ + Each button in .fn-theme-pill carries data-fn-theme-opt="system|light|dark". + Clicking it saves + applies that theme and updates the active highlight. */ + + function updatePillActive() { + const current = computeTheme(); + document.querySelectorAll('[data-fn-theme-opt]').forEach(function (btn) { + const opt = btn.getAttribute('data-fn-theme-opt'); + btn.classList.toggle('is-active', opt === current); + }); + } + + function bindThemeButtons() { + document.querySelectorAll('[data-fn-theme-opt]').forEach(function (btn) { + if (btn.dataset.fnThemeBound === '1') return; + btn.dataset.fnThemeBound = '1'; + btn.addEventListener('click', function () { + const theme = btn.getAttribute('data-fn-theme-opt'); + applyTheme(theme); + saveTheme(theme); + updatePillActive(); + }); + }); + } + + function bindHamburgerButtons() { + document.querySelectorAll('[data-fn-hamburger]').forEach(function (btn) { + if (btn.dataset.fnHamBound === '1') return; + btn.dataset.fnHamBound = '1'; + btn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + toggleMobileMenu(btn); + }); + }); + } + + /* ═════════════════ Desktop search shortcut (⌘K / Ctrl+K) ═════════════════ */ + + var _searchShortcutBound = false; + + function initDesktopSearchShortcut() { + /* Inject ⌘K badge into the search form (once) */ + var form = document.querySelector('.fn-search-wrap .md-search__form'); + if (form && !form.querySelector('.fn-search-kbd')) { + var kbd = document.createElement('span'); + kbd.className = 'fn-search-kbd'; + kbd.setAttribute('aria-hidden', 'true'); + var isMac = /Mac|iPhone|iPod|iPad/.test(navigator.platform || ''); + kbd.innerHTML = isMac + ? 'K' + : 'CtrlK'; + form.appendChild(kbd); + } + + if (_searchShortcutBound) return; + _searchShortcutBound = true; + + document.addEventListener('keydown', function (e) { + /* Only on desktop (search wrap is visible) */ + if (window.innerWidth <= 767) return; + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + var q = getSearchInput(); + if (q) { + q.focus(); + q.select(); + } + } + /* Escape blurs the input */ + if (e.key === 'Escape') { + var q = getSearchInput(); + if (q && document.activeElement === q) q.blur(); + } + }); + } + + /* ═════════════════ Per-page init ═════════════════ */ + + function initPage() { + applyTheme(computeTheme()); // reinforce after body swap + closeMobileMenu(); // reset drawer state on nav + resetMobileSearchState(); // hard-reset after instant-nav body swap + updateActiveTabs(); + updatePillActive(); // sync pill highlight + bindThemeButtons(); + bindHamburgerButtons(); + bindSearchToggleButtons(); + initHero(); + initStickyBar(); + initDesktopSearchShortcut(); + } + + /* ═════════════════ Global (one-time) wiring ═════════════════ + These listeners attach to `document`, which is never swapped + by Material's instant navigation. Survives all DOM replacements. */ + + document.addEventListener('click', function (e) { + const target = e.target; + if (!(target instanceof Element)) return; + + // Search toggle (open/close mobile search bar) + const sTgl = target.closest('[data-fn-search-toggle]'); + if (sTgl) { + e.preventDefault(); + e.stopPropagation(); + if (_mobileSearchOpen) { + closeMobileSearch(); + } else { + closeMobileMenu(); + openMobileSearch(); + } + return; + } + + // Hamburger + const ham = target.closest('[data-fn-hamburger]'); + if (ham) { + e.preventDefault(); + closeMobileSearch(); // close search if open + toggleMobileMenu(ham); + return; + } + + // Mobile nav link → close drawer + const mLink = target.closest('[data-fn-mobile-nav] a'); + if (mLink) { + closeMobileMenu(); + } + + }, { capture: false }); + + /* ═════════════════ Bootstrap ═════════════════ */ + + /* Attach buttons immediately if any already exist (script may load after + parser has seen the header). No-op if not yet in DOM. */ + bindThemeButtons(); + bindHamburgerButtons(); + bindSearchToggleButtons(); + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initPage, { once: true }); + } else { + initPage(); + } + + /* Also re-bind whenever the header subtree is mutated (Material instant + navigation replaces body content; this catches it regardless of whether + document$ is available). */ + const mo = new MutationObserver(function () { + bindThemeButtons(); + bindHamburgerButtons(); + bindSearchToggleButtons(); + }); + if (document.body) { + mo.observe(document.body, { childList: true, subtree: true }); + } else { + document.addEventListener('DOMContentLoaded', function () { + mo.observe(document.body, { childList: true, subtree: true }); + }, { once: true }); + } + + /* Re-run per-page init after each instant-nav DOM swap. + Material exposes `document$` (RxJS-like observable) on the global + scope. We try a few access paths for robustness across versions. */ + function hookDocument$() { + try { + /* Material exposes `window.document$` after bootstrap. */ + const obs = window.document$ || null; + if (obs && typeof obs.subscribe === 'function') { + obs.subscribe(initPage); + return true; + } + } catch (_) {} + return false; + } + + if (!hookDocument$()) { + const iv = setInterval(function () { + if (hookDocument$()) clearInterval(iv); + }, 200); + setTimeout(function () { clearInterval(iv); }, 10000); + } + + /* Final safety net: if document$ is never exposed for some reason, + watch the URL. Material's instant nav does pushState, so popstate + + a MutationObserver on
will catch the swap. */ + let lastPath = location.pathname; + const urlCheck = function () { + if (location.pathname !== lastPath) { + lastPath = location.pathname; + initPage(); + } + }; + window.addEventListener('popstate', urlCheck); + // Patch pushState/replaceState so we can detect programmatic nav. + ['pushState', 'replaceState'].forEach(function (k) { + const orig = history[k]; + history[k] = function () { + const r = orig.apply(this, arguments); + setTimeout(urlCheck, 0); + return r; + }; + }); +})(); diff --git a/docs/stylesheets/fn-layout.css b/docs/stylesheets/fn-layout.css new file mode 100644 index 0000000..3259c08 --- /dev/null +++ b/docs/stylesheets/fn-layout.css @@ -0,0 +1,1428 @@ +/* ═══════════════════════════════════════════════════════════════ + FIVENORTH LAYOUT CSS + Semantic classes for Header, Footer, TabNav, Hero, Products. + Replaces Tailwind utility classes used in the original React code. + ═══════════════════════════════════════════════════════════════ */ + +/* ═══════════════════════════════════════════════════════════════ + GLOBAL SURFACE + Unify html/body and Material containers to fn-surface so there is + no "seam" between our custom header and Material's content areas. + ═══════════════════════════════════════════════════════════════ */ +html, +body { + background-color: var(--fn-surface); + color: var(--fn-fg); + font-family: var(--fn-font-sans); + transition: + background-color var(--fn-duration-base) var(--fn-ease), + color var(--fn-duration-base) var(--fn-ease); +} + +.md-container, +.md-main, +.md-main__inner, +.md-content, +.md-sidebar, +.md-sidebar__scrollwrap { + background-color: var(--fn-surface) !important; + color: var(--fn-fg); +} + +/* Material sidebars use `position: sticky; top: 2.4rem` by default (tuned + to Material's own header). Our sticky action bar is ~54px tall, so we + push the sidebar top offset down to match and prevent overlap when the + action bar is stuck. */ +.md-sidebar { + top: 56px !important; +} +@media (max-width: 959px) { + .md-sidebar { top: 0 !important; } +} + +/* ═══════════════════════════════════════════════════════════════ + HIDE MATERIAL DEFAULT HEADER / TABS / FOOTER + We render our own header/footer below. + ═══════════════════════════════════════════════════════════════ */ +.md-header, +.md-tabs, +.md-footer { + display: none !important; +} + +/* Adjust Material main container to account for our sticky header */ +.md-container { + padding-top: 0 !important; +} + +/* ═══════════════════════════════════════════════════════════════ + HEADER + ═══════════════════════════════════════════════════════════════ */ +/* Brand + sticky are now two separate direct children of . Body is + `display: flex; flex-direction: column; min-height: 100%` (Material's + default), and since spans the entire document height, a sticky + element inside body can bind to the viewport for the full scroll. */ + +/* ── Brand bar (NOT sticky) ───────────────────────────────────── */ +.fn-header__brand { + display: flex; + justify-content: center; + padding: 12px 0; + background-color: var(--fn-surface); + border-bottom: 1px solid var(--fn-line); + flex: 0 0 auto; + align-self: stretch; +} + +[data-md-color-scheme="slate"] .fn-header__brand { + border-bottom-color: #2e323c; +} + +.fn-header__brand-inner { + display: flex; + align-items: center; + gap: 24px; + max-width: var(--fn-max-w); + width: 100%; + min-width: 0; + padding: 0 20px; +} + +/* ── Sticky action bar (tabs + search + theme + hamburger) ───── */ +.fn-header__sticky { + display: flex; + justify-content: center; + position: sticky; + top: 0; + z-index: 50; + padding: 10px 0; + background-color: var(--fn-surface-overlay); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-bottom: 1px solid transparent; + transition: border-color var(--fn-duration-fast) var(--fn-ease); + /* Critical for sticky-inside-flex-column: ensure this bar does NOT + stretch vertically — flex item would otherwise be stretched and + sticky behavior becomes unpredictable. */ + flex: 0 0 auto; + align-self: stretch; +} + +/* Subtle divider + shadow becomes visible only when actually stuck */ +.fn-header__sticky.is-stuck { + border-bottom-color: var(--fn-line-muted); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} + +[data-md-color-scheme="slate"] .fn-header__sticky.is-stuck { + box-shadow: none; +} + +.fn-header__sticky-inner { + display: flex; + align-items: center; + justify-content: space-between; + max-width: var(--fn-max-w); + width: 100%; + gap: 16px; + padding: 0 20px; +} + +/* Mobile nav drawer also needs flex-item sizing */ +.fn-mobile-nav { + flex: 0 0 auto; + align-self: stretch; +} + +.fn-logo-link { + display: inline-block; + flex: 0 0 auto; +} + +.fn-logo-img { + display: block; + height: 32px; + width: 132px; + max-width: 100%; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + vertical-align: middle; +} + +@media (max-width: 767px) { + .fn-header__brand { + position: sticky; + top: 0; + z-index: 50; + } + .fn-header__sticky { + display: none; + } +} + +.fn-header__right { + display: flex; + align-items: center; + gap: 12px; + position: relative; + z-index: 5; +} + +/* Neutralize Material's global click-blocker overlay. We render search as a + header dropdown (not fullscreen), so this overlay should never catch clicks. */ +.md-overlay { + display: none !important; + pointer-events: none !important; +} + +/* ── Clipboard / share toast notification ────────────────────── + Material uses inverted --md-default-fg/bg-color for .md-dialog, which + clashes with our custom tokens. Override with explicit high-contrast + values for both light and dark modes so the "Copied" message is always + readable. */ +.md-dialog { + background-color: #1e1e24 !important; + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4) !important; + border-radius: 8px !important; + padding: 8px 14px !important; +} + +.md-dialog__inner { + color: #f1f5f9 !important; + font-size: 0.75rem !important; + font-weight: 500; +} + +/* ═══════════════════════════════════════════════════════════════ + SEARCH WRAP + .fn-search-wrap is a direct body-level sibling of .fn-header__sticky + (no backdrop-filter ancestor → position:fixed is viewport-relative). + + Desktop: fixed at top-right of sticky bar. + Mobile: hidden off-screen (NOT display:none so Material can init); + slides in full-width when body.fn-mobile-search-active. + ═══════════════════════════════════════════════════════════════ */ +.fn-search-wrap { + position: fixed; + top: 0; + right: max(20px, calc((100vw - var(--fn-max-w)) / 2 + 20px)); + z-index: 55; + display: flex; + align-items: center; + height: 56px; + pointer-events: auto; +} + +@media (max-width: 767px) { + /* NOT display:none — keep in DOM so Material initialises the search worker. + Pushed off-screen to the right so it's invisible and non-interactive. */ + .fn-search-wrap { + right: -120vw; + pointer-events: none; + transition: right 0s; /* snap, no slide glitch on desktop */ + } + + /* ── Active: full-width search bar ── */ + body.fn-mobile-search-active .fn-search-wrap { + right: 0 !important; + left: 0 !important; + top: 0 !important; + height: 56px !important; + pointer-events: auto !important; + z-index: 300 !important; + background: var(--fn-surface) !important; + border-bottom: 1px solid var(--fn-line) !important; + padding: 0 12px 0 16px !important; + gap: 8px !important; + } + + /* Expand the form to fill the bar */ + body.fn-mobile-search-active .fn-search-wrap .md-search__form { + flex: 1 !important; + width: auto !important; + min-width: 0 !important; + height: 40px !important; + max-width: none !important; + font-size: 16px !important; /* prevents iOS auto-zoom on focus */ + background: var(--fn-surface-inset) !important; + border: 1.5px solid var(--fn-line) !important; + border-radius: var(--fn-radius-md) !important; + display: flex !important; + align-items: center !important; + padding: 0 !important; + } + + body.fn-mobile-search-active .fn-search-wrap .md-search__form:focus-within { + width: auto !important; + border-color: var(--fn-accent) !important; + } + + body.fn-mobile-search-active .fn-search-wrap .md-search__input { + font-size: 16px !important; + color: var(--fn-fg) !important; + background: transparent !important; + caret-color: var(--fn-accent) !important; + opacity: 1 !important; + pointer-events: auto !important; + width: 100% !important; + min-width: 0 !important; + padding: 0 12px !important; + text-align: left !important; + } + + /* Hide Material's icon label (has 2 SVGs that render weirdly on mobile) */ + body.fn-mobile-search-active .fn-search-wrap .md-search__icon[for="__search"] { + display: none !important; + } + + /* Hide all buttons inside the input (share + reset) — close is outside */ + body.fn-mobile-search-active .fn-search-wrap .md-search__options { + display: none !important; + } + + /* Custom search icon injected by JS — flex item inside the form row */ + .fn-mobile-search-icon { + display: none; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + margin-left: 12px; + color: var(--fn-fg-muted); + pointer-events: none; + } + body.fn-mobile-search-active .fn-mobile-search-icon { + display: flex; + } + + /* Style the close (X) button */ + .fn-mobile-search-close { + flex-shrink: 0 !important; + width: 36px !important; + height: 36px !important; + border-radius: var(--fn-radius-md) !important; + background: transparent !important; + color: var(--fn-fg-secondary) !important; + border: none !important; + margin-left: 0 !important; + } + .fn-mobile-search-close:hover { color: var(--fn-fg) !important; } + + /* Results dropdown — force visible + full-width below the 60px bar. + Use position:fixed (not absolute) so the output escapes fn-search-wrap's + stacking context and sits in the root stacking context at z-index 9000, + above everything regardless of any parent overflow or stacking issues. */ + body.fn-mobile-search-active .fn-search-wrap .md-search__output { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + pointer-events: auto !important; + position: fixed !important; + top: 60px !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100% !important; + height: auto !important; + max-height: calc(100dvh - 60px) !important; + border-radius: 0 !important; + box-shadow: none !important; + transform: none !important; + transition: none !important; + background: var(--fn-surface-raised) !important; + border-top: 1px solid var(--fn-line) !important; + overflow-y: auto !important; + z-index: 9000 !important; + } + + body.fn-mobile-search-active .fn-search-wrap .md-search__scrollwrap { + height: auto !important; + max-height: calc(100dvh - 60px) !important; + overflow-y: auto !important; + } + + /* Ensure result items are clearly visible on mobile */ + body.fn-mobile-search-active .fn-search-wrap .md-search-result { + background: transparent !important; + } + body.fn-mobile-search-active .fn-search-wrap .md-search-result__meta { + color: var(--fn-fg-muted) !important; + background: var(--fn-surface-raised) !important; + } + body.fn-mobile-search-active .fn-search-wrap .md-search-result__item { + background: transparent !important; + } + body.fn-mobile-search-active .fn-search-wrap .md-search-result__link { + color: var(--fn-fg) !important; + } + body.fn-mobile-search-active .fn-search-wrap .md-search-result__title { + color: var(--fn-fg) !important; + } + body.fn-mobile-search-active .fn-search-wrap .md-search-result__teaser { + color: var(--fn-fg-muted) !important; + } + + /* Base styles for the close button (overrides applied above in active state) */ + .fn-mobile-search-close { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 0; + appearance: none; + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; + } + .fn-mobile-search-close svg { pointer-events: none; } +} + +/* Contain Material's .md-search inside our wrapper. Material itself has + rules that float the form to the right on small screens; we neutralize + those because our wrapper sits inside a flex row. */ +.fn-search-wrap .md-search { + position: static !important; + padding: 0 !important; +} + +/* Kill ALL of Material's mobile/desktop overrides on .md-search__inner + so it renders inline inside our flex wrapper. + Material applies: position:fixed; left:100%; opacity:0; pointer-events:none + on small screens — we override everything with !important. */ +.fn-search-wrap .md-search__inner { + position: static !important; + top: auto !important; + left: auto !important; + right: auto !important; + bottom: auto !important; + width: auto !important; + height: auto !important; + margin: 0 !important; + padding: 0 !important; + float: none !important; + transform: none !important; + transition: none !important; + opacity: 1 !important; + pointer-events: auto !important; +} + +.fn-search-wrap .md-search__form { + position: relative; + display: flex; + align-items: center; + height: 32px; + width: 220px; + padding: 0 12px 0 32px; + border-radius: var(--fn-radius-md); + background: var(--fn-surface-inset); + border: 1px solid var(--fn-line); + box-shadow: none; + transition: + width 0.2s cubic-bezier(0.1, 0.7, 0.1, 1), + background-color var(--fn-duration-fast) var(--fn-ease), + border-color var(--fn-duration-fast) var(--fn-ease); +} + +.fn-search-wrap .md-search__form:hover { + border-color: var(--fn-line-divider); +} + +.fn-search-wrap .md-search__form:focus-within { + width: 360px; + border-color: var(--fn-accent); +} + +/* Search input */ +.fn-search-wrap .md-search__input { + position: static; + flex: 1; + min-width: 0; + height: 100%; + padding: 0; + background: transparent; + color: var(--fn-fg); + font-size: 12px; + font-family: inherit; + border: 0; + outline: none; + text-align: left !important; +} + +.fn-search-wrap .md-search__input::placeholder { + color: var(--fn-fg-muted); + opacity: 1; +} + +/* Search icon (magnifier) on the left */ +.fn-search-wrap .md-search__icon[for="__search"] { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + width: 16px; + height: 16px; + margin: 0; + padding: 0; + color: var(--fn-fg-muted); + cursor: text; +} + +.fn-search-wrap .md-search__icon[for="__search"] svg { + width: 16px; + height: 16px; +} + +/* Hide the "previous" (back arrow) icon that Material swaps in on mobile */ +.fn-search-wrap .md-search__icon[for="__search"] svg:nth-child(2) { + display: none; +} + +/* ⌘K / Ctrl+K badge inside the search input */ +.fn-search-kbd { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + pointer-events: none; + margin-right: 2px; +} + +.fn-search-kbd kbd { + display: inline-flex; + align-items: center; + justify-content: center; + height: 18px; + min-width: 18px; + padding: 0 4px; + border-radius: 4px; + border: 1px solid var(--fn-line-divider); + background: var(--fn-surface-raised); + color: var(--fn-fg-muted); + font-size: 10px; + font-family: inherit; + line-height: 1; + font-style: normal; +} + +/* Hide badge when input has focus (user is typing) */ +.fn-search-wrap .md-search__form:focus-within .fn-search-kbd { + display: none; +} + +@media (max-width: 767px) { + .fn-search-kbd { display: none !important; } +} + +/* Options bar (share / reset) — keep reset button accessible on the right */ +.fn-search-wrap .md-search__options { + position: absolute; + right: 4px; + top: 0; + bottom: 0; + display: flex; + align-items: center; +} + +.fn-search-wrap .md-search__options > .md-icon { + width: 20px; + height: 20px; + padding: 0; + color: var(--fn-fg-muted); + opacity: 0; + pointer-events: none; + transition: opacity var(--fn-duration-fast) var(--fn-ease); +} + +.fn-search-wrap [data-md-toggle="search"]:checked ~ .md-header .md-search__options > .md-icon, +.fn-search-wrap .md-search__form:focus-within .md-search__options > .md-icon { + opacity: 1; + pointer-events: auto; +} + +/* The fullscreen overlay that Material dims the page with — hide entirely. + We use a dropdown (.md-search__output) instead of the fullscreen search + UX. Use !important because Material has multiple media-query rules that + re-assert position/size on this element. If it leaks visible it becomes + an invisible click-trap covering the theme button and surrounding area. */ +.fn-search-wrap .md-search__overlay, +.md-search__overlay, +label.md-search__overlay { + display: none !important; + visibility: hidden !important; + width: 0 !important; + height: 0 !important; + pointer-events: none !important; +} + +/* Results dropdown — float below the input as a dropdown, not fullscreen. + display:block !important prevents Material's mobile CSS from hiding it + (Material uses display:none on small screens via its own media queries). + We control visibility via opacity/pointer-events only. */ +.fn-search-wrap .md-search__output { + display: block !important; + visibility: visible !important; + position: absolute; + top: calc(100% + 6px); + right: 0; + width: min(520px, 90vw); + max-height: 70vh; + border-radius: var(--fn-radius-md); + background: var(--fn-surface-raised); + border: 1px solid var(--fn-line); + box-shadow: 0 12px 40px -8px rgba(0, 0, 0, 0.25); + overflow: hidden; + z-index: 60; + /* Hidden by default — only via opacity, NOT display */ + opacity: 0; + transform: translateY(-4px); + pointer-events: none; + transition: + opacity var(--fn-duration-fast) var(--fn-ease), + transform var(--fn-duration-fast) var(--fn-ease); +} + +/* Shown when: Material's search toggle is checked, OR the input has focus. + Material emits `#__search` (data-md-toggle="search") at body level, + which is a PRECEDING SIBLING of .fn-search-wrap, so `~` works here. Our + JS checks that same element on mobile open so Material's search JS also + activates result routing. */ +.fn-search-wrap:focus-within .md-search__output { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.fn-search-wrap .md-search__scrollwrap { + max-height: 70vh; + overflow-x: hidden; + overflow-y: auto; + background: transparent; + width: 100%; +} + +.fn-search-wrap .md-search-result { + background: transparent; + color: var(--fn-fg); +} + +.fn-search-wrap .md-search-result__meta { + padding: 12px 16px; + font-size: 12px; + color: var(--fn-fg-muted); + background: transparent; + border-bottom: 1px solid var(--fn-line-muted); +} + +.fn-search-wrap .md-search-result__list { + margin: 0; + padding: 4px 0; + list-style: none; +} + +.fn-search-wrap .md-search-result__item { + box-shadow: none; + border-bottom: 1px solid var(--fn-line-muted); +} + +.fn-search-wrap .md-search-result__item:last-child { + border-bottom: none; +} + +.fn-search-wrap .md-search-result__link { + padding: 12px 16px !important; + background: transparent; + color: var(--fn-fg); + transition: background-color var(--fn-duration-fast) var(--fn-ease); +} + +.fn-search-wrap .md-search-result__article { + overflow: hidden; +} + +.fn-search-wrap .md-search-result__link:hover, +.fn-search-wrap .md-search-result__link:focus, +.fn-search-wrap .md-search-result__link[data-md-state="active"] { + background: var(--fn-surface-inset); +} + +.fn-search-wrap .md-search-result__article--document .md-search-result__title { + font-size: 14px; + font-weight: 600; + color: var(--fn-fg); + margin: 0; +} + +.fn-search-wrap .md-search-result__teaser { + font-size: 12px; + color: var(--fn-fg-secondary); + margin-top: 4px; + max-height: none; +} + +.fn-search-wrap .md-search-result mark { + color: var(--fn-accent); + background: transparent; + font-weight: 600; +} + +/* Hide suggest div's default positioning quirks */ +.fn-search-wrap .md-search__suggest { + opacity: 0; +} + +/* ── Theme toggle button ───────────────────────────────────── */ +/* ── Theme pill (System / Light / Dark) ────────────────────── + A capsule with 3 icon buttons. The active option gets a filled + background; others are ghost. Lives in the footer bottom bar. */ +.fn-theme-pill { + display: flex; + align-items: center; + gap: 2px; + padding: 3px; + background: var(--fn-surface-inset); + border: 1px solid var(--fn-line); + border-radius: 999px; + flex-shrink: 0; +} + +.fn-theme-pill__btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 999px; + border: none; + background: transparent; + color: var(--fn-fg-muted); + cursor: pointer; + padding: 0; + margin: 0; + font: inherit; + line-height: 0; + appearance: none; + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; + transition: + background-color 0.15s ease, + color 0.15s ease; +} + +.fn-theme-pill__btn svg { + pointer-events: none; + display: block; +} + +/* Active state */ +.fn-theme-pill__btn.is-active { + background: var(--fn-accent); + color: #fff; +} + +/* Hover on inactive buttons */ +.fn-theme-pill__btn:not(.is-active):hover { + background: var(--fn-surface-raised); + color: var(--fn-fg); +} + +.fn-theme-pill__btn:focus-visible { + outline: 2px solid var(--fn-accent); + outline-offset: 2px; +} + +/* ── Hamburger button ──────────────────────────────────────── */ +.fn-hamburger { + position: relative; + box-sizing: border-box; + width: 32px; + height: 32px; + border-radius: var(--fn-radius-md); + border: 1px solid var(--fn-line); + background: var(--fn-surface-inset); + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + padding: 0; + margin: 0; + flex-shrink: 0; + z-index: 1; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; + appearance: none; + -webkit-appearance: none; + transition: border-color var(--fn-duration-fast) var(--fn-ease); +} + +.fn-hamburger > span, +.fn-hamburger > span::before, +.fn-hamburger > span::after { + pointer-events: none; +} + +/* Invisible hit-area expansion for hamburger */ +.fn-hamburger::before { + content: ''; + position: absolute; + inset: -8px; + border-radius: calc(var(--fn-radius-md) + 8px); +} + +.fn-hamburger:hover { border-color: var(--fn-accent); } +.fn-hamburger:active { transform: scale(0.96); } +.fn-hamburger:focus-visible { + outline: 2px solid var(--fn-accent); + outline-offset: 2px; +} + +.fn-hamburger-line, +.fn-hamburger-line::before, +.fn-hamburger-line::after { + display: block; + width: 14px; + height: 1.5px; + background: var(--fn-fg-secondary); + border-radius: 1px; + transition: + transform var(--fn-duration-base) var(--fn-ease), + opacity var(--fn-duration-base) var(--fn-ease), + background-color var(--fn-duration-base) var(--fn-ease); +} + +.fn-hamburger-line { position: relative; } + +.fn-hamburger-line::before, +.fn-hamburger-line::after { + content: ''; + position: absolute; + left: 0; +} + +.fn-hamburger-line::before { transform: translateY(-4.5px); } +.fn-hamburger-line::after { transform: translateY(4.5px); } + +.fn-hamburger-line.fn-hamburger-open { + background: transparent; +} + +.fn-hamburger-line.fn-hamburger-open::before { + transform: rotate(45deg); +} + +.fn-hamburger-line.fn-hamburger-open::after { + transform: rotate(-45deg); +} + +/* ── Mobile nav drawer ─────────────────────────────────────── */ +.fn-mobile-nav { + display: none; +} + +/* Hamburger in brand bar — only on mobile, pushed right via margin-left: auto */ +.fn-hamburger--brand { + display: none; +} + +/* Mobile controls group (search icon + hamburger) */ +.fn-mobile-controls { + display: none; + align-items: center; + gap: 8px; + margin-left: auto; +} + +.fn-mobile-search-btn { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: var(--fn-radius-md); + border: 1px solid var(--fn-line); + background: var(--fn-surface-inset); + color: var(--fn-fg-secondary); + cursor: pointer; + padding: 0; + flex-shrink: 0; + appearance: none; + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; + transition: border-color var(--fn-duration-fast) var(--fn-ease), + color var(--fn-duration-fast) var(--fn-ease); +} + +.fn-mobile-search-btn svg { pointer-events: none; } +.fn-mobile-search-btn:hover { border-color: var(--fn-accent); color: var(--fn-accent); } +.fn-mobile-search-btn.is-active { border-color: var(--fn-accent); color: var(--fn-accent); } + +/* Mobile controls and nav */ +@media (max-width: 767px) { + .fn-mobile-controls { display: flex; } + .fn-hamburger--brand { display: flex; } + + /* Mobile nav drawer — fixed below the sticky brand bar */ + .fn-mobile-nav { + position: fixed; + top: 61px; /* brand bar: 16px padding-top + 32px logo + 12px padding-bottom + 1px border */ + left: 0; + right: 0; + z-index: 49; + background: var(--fn-surface); + border-bottom: 1px solid transparent; + display: grid; + grid-template-rows: 0fr; + transition: + grid-template-rows var(--fn-duration-base) var(--fn-ease-out), + border-color var(--fn-duration-fast) var(--fn-ease); + overflow: hidden; + } + + .fn-mobile-nav-open { + grid-template-rows: 1fr; + border-bottom-color: var(--fn-line); + } + + .fn-mobile-nav > div { + overflow: hidden; + } + + .fn-mobile-nav__list { + display: flex; + flex-direction: column; + gap: 4px; + padding: 16px 20px 20px; + } + + .fn-mobile-nav__link { + display: flex; + align-items: center; + border-radius: var(--fn-radius-md); + padding: 10px 16px; + font-size: 14px; + font-weight: 500; + color: var(--fn-fg); + text-decoration: none; + border: 1px solid transparent; + background: transparent; + transition: + color var(--fn-duration-fast) var(--fn-ease), + border-color var(--fn-duration-fast) var(--fn-ease); + } + + .fn-mobile-nav__link:hover { + border-color: var(--fn-line); + color: var(--fn-fg-secondary); + } + + .fn-mobile-nav__link.is-active { + border-color: var(--fn-line); + color: var(--fn-accent); + } +} + +/* ═══════════════════════════════════════════════════════════════ + TAB NAV (pill row) + ═══════════════════════════════════════════════════════════════ */ +.fn-tabnav { + display: flex; + flex-wrap: wrap; + gap: 4px; + min-width: 0; + flex: 1 1 auto; +} + +@media (max-width: 767px) { + .fn-tabnav { display: none; } +} + +.fn-tab { + align-content: center; + border-radius: 9999px; + padding: 8px 24px; + cursor: pointer; + color: var(--fn-fg); + text-decoration: none; + border: 1px solid transparent; + background: transparent; + transition: + color var(--fn-duration-fast) var(--fn-ease), + border-color var(--fn-duration-fast) var(--fn-ease); +} + +.fn-tab:hover { + border-color: var(--fn-line); + color: var(--fn-fg-secondary); +} + +.fn-tab.is-active { + border-color: var(--fn-line); + color: var(--fn-accent); +} + +.fn-tab__label { + font-size: 14px; + font-weight: 500; + text-align: center; + width: max-content; +} + +/* ═══════════════════════════════════════════════════════════════ + PAGE LAYOUT WRAPPERS + ═══════════════════════════════════════════════════════════════ */ +.fn-page { + font-synthesis: none; + display: flex; + flex-direction: column; + width: 100%; + min-height: 100vh; + background: var(--fn-surface); + color: var(--fn-fg); + -webkit-font-smoothing: antialiased; + font-size: 12px; + line-height: 1rem; +} + +.fn-main { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 24px 20px 54px 20px; + gap: 50px; +} + +@media (max-width: 1023px) { + .fn-main { + padding-top: 0; + } +} + +.fn-main__inner { + display: flex; + flex-direction: column; + max-width: var(--fn-max-w); + width: 100%; + gap: 112px; +} + +@media (max-width: 767px) { + .fn-main__inner { + gap: 32px; + } +} + +/* ═══════════════════════════════════════════════════════════════ + HERO SECTION + ═══════════════════════════════════════════════════════════════ */ +.fn-hero { + position: relative; +} + +.fn-hero__text { + display: flex; + flex-direction: column; + max-width: 800px; + gap: 15px; +} + +.fn-hero__title { + position: relative; + letter-spacing: -1px; + color: var(--fn-fg); + font-weight: 700; + font-size: 54px; + line-height: 120%; + max-width: 700px; + padding-bottom: 0; + margin: 0; + font-family: var(--fn-font-display); +} + +@media (max-width: 1023px) { + .fn-hero__title { font-size: 42px; line-height: 52px; } +} + +@media (max-width: 639px) { + .fn-hero__title { font-size: 32px; line-height: 40px; margin-top: 20px; } +} + +.fn-hero__subtitle { + max-width: 600px; + color: var(--fn-fg-secondary); + font-size: 18px; + line-height: 160%; +} + +.fn-hero__bg-wrap { + position: absolute; + top: -40px; + right: 0; + width: 441px; + height: 441px; + pointer-events: none; +} + +@media (max-width: 1023px) { + .fn-hero__bg-wrap { display: none; } +} + +/* Mobile grid — visible only on small screens, above h1 */ +.fn-hero__bg-wrap--mobile { + position: relative; + top: auto; + right: auto; + width: 100%; + /* aspect-ratio 7:6 = 7 cols × 6 rows → cells are perfectly square */ + aspect-ratio: 7 / 6; + height: auto; + margin-bottom: 16px; + display: none; +} + +@media (max-width: 1023px) { + .fn-hero__bg-wrap--mobile { + display: block; + } +} + +.fn-hero__bg-wrap--mobile .fn-hero-grid { + grid-template-columns: repeat(7, 1fr); + grid-template-rows: repeat(6, 1fr); + width: 100%; + height: 100%; +} + +/* ── Hero grid (animated) ──────────────────────────────────── */ +.fn-hero-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + grid-template-rows: repeat(7, 1fr); + width: 100%; + height: 100%; + clip-path: inset(-1px); +} + +.fn-grid-cell { + position: relative; + opacity: 0; +} + +.fn-grid-cell::before { + content: ''; + position: absolute; + inset: -0.5px; + border: 1px solid var(--fn-grid-line, var(--fn-line)); + pointer-events: none; + z-index: 1; +} + +.fn-grid-cell::after { + content: ''; + position: absolute; + inset: 0; + background-color: var(--cell-color); + opacity: var(--fn-cell-idle); + filter: var(--fn-cell-filter-idle); +} + +.fn-grid-cell-active::after { + opacity: var(--fn-cell-idle-active); +} + +/* Phase: drop */ +.fn-phase-drop .fn-grid-cell { + animation: fn-cell-drop 0.55s var(--fn-ease-out) calc(var(--drop-i, 0) * 120ms) forwards; +} + +@keyframes fn-cell-drop { + 0% { opacity: 0; transform: translate3d(0, -300px, 0); } + 30% { opacity: 1; } + 75% { transform: translate3d(0, 4px, 0); } + 90% { transform: translate3d(0, -2px, 0); } + 100% { opacity: 1; transform: translate3d(0, 0, 0); } +} + +/* Phase: blink */ +.fn-phase-blink .fn-grid-cell { + opacity: 1; +} + +.fn-phase-blink .fn-grid-cell-active { + animation: fn-cell-glow 9s ease-in-out calc(var(--cell-i, 0) * 1s) both; +} + +.fn-phase-blink .fn-grid-cell-active::after { + animation: fn-cell-fill 9s ease-in-out calc(var(--cell-i, 0) * 1s) both; +} + +@keyframes fn-cell-fill { + 0%, 14%, 100% { + opacity: var(--fn-cell-idle-active); + filter: var(--fn-cell-filter-idle); + } + 5%, 9% { + opacity: 1; + filter: var(--fn-cell-filter-glow); + } +} + +@keyframes fn-cell-glow { + 0%, 14%, 100% { + box-shadow: + inset 0 0 0 0 transparent, + 0 0 0 0 transparent; + } + 5%, 9% { + box-shadow: + inset 0 0 12px var(--fn-accent-glow), + 0 0 var(--fn-cell-halo-blur) var(--fn-cell-halo-spread) + color-mix(in srgb, var(--cell-color) var(--fn-cell-halo-strength), transparent); + } +} + +/* Phase: fadeout */ +.fn-phase-fadeout .fn-grid-cell { + opacity: 1; + animation: fn-cell-fadeout 0.8s var(--fn-ease) forwards; +} + +@keyframes fn-cell-fadeout { + to { opacity: 0; transform: scale(0.92); } +} + +/* Phase: pause */ +.fn-phase-pause .fn-grid-cell { + opacity: 0; +} + +/* ═══════════════════════════════════════════════════════════════ + PRODUCTS GRID + ═══════════════════════════════════════════════════════════════ */ +.fn-products { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(500px, 1fr)); + width: 100%; + gap: 24px; +} + +@media (max-width: 767px) { + .fn-products { + grid-template-columns: 1fr; + gap: 12px; + } +} + +.fn-card { + display: flex; + flex-direction: column; + gap: 24px; + padding: 48px; + border-radius: var(--fn-radius-md); + background: var(--fn-surface-raised); + border: 1px solid var(--fn-line); + text-decoration: none; + color: inherit; + transition: + transform var(--fn-duration-slow) var(--fn-ease-out), + border-color var(--fn-duration-slow) var(--fn-ease), + box-shadow var(--fn-duration-slow) var(--fn-ease), + background-color var(--fn-duration-base) var(--fn-ease), + color var(--fn-duration-base) var(--fn-ease); +} + +@media (max-width: 767px) { + .fn-card { + padding: 20px; + gap: 14px; + } +} + +.fn-card:hover { + transform: translateY(-6px); + border-color: var(--fn-accent-muted); + box-shadow: var(--fn-shadow-hover); +} + +.fn-card__icon-wrap { + display: flex; + align-items: center; + justify-content: center; + height: 100px; + width: 100px; + border-radius: 16px; + flex-shrink: 0; +} + +@media (max-width: 767px) { + .fn-card__icon-wrap { + height: 72px; + width: 72px; + border-radius: 12px; + } +} + +.fn-card__icon { + display: block; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + flex-shrink: 0; +} + +.fn-card__body { + display: flex; + flex-direction: column; + gap: 8px; +} + +.fn-card__title { + letter-spacing: -0.48px; + font-weight: 700; + font-size: 24px; + line-height: 33.6px; + color: var(--fn-card-t-light, var(--fn-fg)); + transition: color var(--fn-duration-base) var(--fn-ease); +} + +[data-md-color-scheme="slate"] .fn-card__title { + color: var(--fn-card-t-dark, var(--fn-fg)); +} + +/* Per-card icon sizes (mirrors `iconClass` in src/site.ts) */ +.fn-card__icon--loop-sdk { height: 62px; width: 74px; } +.fn-card__icon--id-sdk { height: 20px; width: 70px; } +.fn-card__icon--5n-infra { height: 72px; width: 72px; background-size: cover; } +.fn-card__icon--5n-dashboard { height: 50px; width: 46px; } +.fn-card__icon--5n-time-machine{ height: 44px; width: 44px; } +.fn-card__icon--coming-soon { height: 36px; width: 36px; } + +.fn-card__desc { + color: var(--fn-fg-secondary); + font-size: 16px; + line-height: 25.6px; +} + +.fn-card__cta { + display: flex; + align-items: center; + margin-top: auto; + gap: 8px; +} + +.fn-card__cta-label { + color: var(--fn-fg); + font-weight: 700; + font-size: 14px; + line-height: 16.8px; + flex-shrink: 0; +} + +.fn-card__cta-icon { + display: block; + width: 20px; + height: 20px; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + flex-shrink: 0; + filter: var(--fn-logo-filter); +} + +/* ═══════════════════════════════════════════════════════════════ + FOOTER + ═══════════════════════════════════════════════════════════════ */ +.fn-footer { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 64px 20px 32px 20px; + background: var(--fn-surface-section); + border-top: 1px solid var(--fn-line-muted); +} + +.fn-footer__inner { + display: flex; + flex-direction: column; + max-width: var(--fn-max-w); + width: 100%; + gap: 48px; + padding: 0 20px; +} + +.fn-footer__top { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 32px; +} + +.fn-footer__links { + display: flex; + gap: 24px; + flex-wrap: wrap; +} + +.fn-footer__link { + font-size: 14px; + color: var(--fn-fg-secondary); + text-decoration: none; + transition: color var(--fn-duration-fast) var(--fn-ease); +} + +.fn-footer__link:hover { + color: var(--fn-fg); +} + +.fn-footer__bottom { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 32px; + border-top: 1px solid var(--fn-line-muted); +} + +.fn-footer__copyright { + font-size: 12px; + letter-spacing: 0.24px; + color: var(--fn-fg-muted); +} + +/* ═══════════════════════════════════════════════════════════════ + DOCS PAGE TWEAKS + ═══════════════════════════════════════════════════════════════ */ + +/* Keep docs pages breathing room under the sticky header */ +.md-main { + padding-top: 0.6rem; +} + +/* Align Material's text on content pages with fn tokens */ +.md-typeset { + color: var(--fn-fg); +} + +.md-typeset h1, +.md-typeset h2, +.md-typeset h3, +.md-typeset h4, +.md-typeset h5, +.md-typeset h6 { + color: var(--fn-fg); +} + +.md-nav__link, +.md-nav__title { + color: var(--fn-fg-secondary); +} + +.md-nav__link:hover, +.md-nav__link--active { + color: var(--fn-accent); +} diff --git a/docs/stylesheets/fn-tokens.css b/docs/stylesheets/fn-tokens.css new file mode 100644 index 0000000..0eafdf1 --- /dev/null +++ b/docs/stylesheets/fn-tokens.css @@ -0,0 +1,157 @@ +/* ═══════════════════════════════════════════════════════════════ + FIVENORTH DESIGN TOKENS + Ported from 5n-fivenorth-io/src/app.css. + Integrates with MkDocs Material palette via [data-md-color-scheme]. + ═══════════════════════════════════════════════════════════════ */ + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Inter+Tight:wght@800;900&display=swap'); + +/* ── Primitive tokens (constant, theme-independent) ────────── */ +:root { + --fn-accent: #407AEF; + --fn-accent-muted: rgba(64, 122, 239, 0.35); + --fn-accent-glow: rgba(64, 122, 239, 0.15); + + --fn-radius-md: 8px; + + --fn-duration-fast: 200ms; + --fn-duration-base: 350ms; + --fn-duration-slow: 400ms; + + --fn-ease: ease; + --fn-ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); + + --fn-shadow-hover: 0 12px 40px -8px var(--fn-accent-glow); + + --fn-max-w: 61rem; /* matches Material's .md-grid max-width exactly */ + + --fn-font-sans: 'Inter', system-ui, sans-serif; + --fn-font-display: 'Inter Tight', 'Inter', system-ui, sans-serif; +} + +/* ── Semantic tokens · Light (Material "default") ──────────── */ +[data-md-color-scheme="default"] { + --fn-surface: #FFFFFF; + --fn-surface-raised: #F9FAFB; + --fn-surface-overlay: rgba(255, 255, 255, 0.8); + --fn-surface-inset: #F3F4F6; + --fn-surface-section: #F9FAFB; + + --fn-fg: #111827; + --fn-fg-secondary: #6B7280; + --fn-fg-muted: #9CA3AF; + + --fn-line: #E5E7EB; + --fn-line-muted: #E5E7EB; + --fn-line-divider: #D1D5DB; + + --fn-grid-line: #C9CDD4; + + --fn-cell-idle: 0.35; + --fn-cell-idle-active: 0.45; + --fn-cell-filter-idle: saturate(1) brightness(1); + --fn-cell-filter-glow: saturate(1.1) brightness(0.93); + --fn-cell-halo-blur: 22px; + --fn-cell-halo-spread: 3px; + --fn-cell-halo-strength: 65%; + + --fn-logo-filter: brightness(0); +} + +/* ── Semantic tokens · Dark (Material "slate") ─────────────── */ +[data-md-color-scheme="slate"] { + --fn-surface: #111113; + --fn-surface-raised: #141517; + --fn-surface-overlay: rgba(17, 17, 19, 0.8); + --fn-surface-inset: #1B1B1D; + --fn-surface-section: #111113; + + --fn-fg: #F8FAFC; + --fn-fg-secondary: #94A3B8; + --fn-fg-muted: #666666; + + --fn-line: #21242B; + --fn-line-muted: #191B21; + --fn-line-divider: #272626; + + --fn-grid-line: #21242B; + + --fn-cell-idle: 0.08; + --fn-cell-idle-active: 0.15; + --fn-cell-filter-idle: saturate(1) brightness(1); + --fn-cell-filter-glow: saturate(1) brightness(1); + --fn-cell-halo-blur: 10px; + --fn-cell-halo-spread: 1px; + --fn-cell-halo-strength: 30%; + + --fn-logo-filter: none; + + /* ── Material palette overrides for slate scheme ────────────── + Because we disabled Material's built-in `palette:` to keep our + custom JS as the single source of truth, we need to remap + Material's own CSS variables to our dark tokens — otherwise + sticky bars (.md-nav__title), admonitions, code blocks, etc. + keep their default LIGHT values and bleed through in dark mode + (the "white streak" at the top of the sidebars). */ + --md-default-bg-color: #111113; + --md-default-bg-color--light: rgba(17, 17, 19, 0.7); + --md-default-bg-color--lighter:rgba(17, 17, 19, 0.3); + --md-default-bg-color--lightest:rgba(17, 17, 19, 0.12); + + --md-default-fg-color: #F8FAFC; + --md-default-fg-color--light: #94A3B8; + --md-default-fg-color--lighter:#666666; + --md-default-fg-color--lightest:#2E333C; + + --md-primary-fg-color: #407AEF; + --md-primary-bg-color: #F8FAFC; + --md-accent-fg-color: #407AEF; + --md-accent-bg-color: #F8FAFC; + + --md-typeset-color: #F8FAFC; + --md-typeset-a-color: #407AEF; + --md-typeset-table-color: rgba(255, 255, 255, 0.12); + --md-typeset-table-color--light:rgba(255, 255, 255, 0.035); + --md-typeset-mark-color: rgba(64, 122, 239, 0.28); + + --md-code-fg-color: #E2E8F0; + --md-code-bg-color: #1B1B1D; + --md-code-hl-color: rgba(64, 122, 239, 0.3); + + --md-admonition-fg-color: #F8FAFC; + --md-admonition-bg-color: #141517; + + --md-footer-fg-color: #F8FAFC; + --md-footer-fg-color--light: #94A3B8; + --md-footer-fg-color--lighter: #666666; + --md-footer-bg-color: #111113; + --md-footer-bg-color--dark: #0A0A0C; +} + +/* ── Global element smoothing ──────────────────────────────── */ +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +html { scroll-behavior: smooth; } + +/* Smooth theme transition for all themed elements */ +.fn-t { + transition: + background-color var(--fn-duration-base) var(--fn-ease), + color var(--fn-duration-base) var(--fn-ease), + border-color var(--fn-duration-base) var(--fn-ease), + filter var(--fn-duration-base) var(--fn-ease); +} + +/* Logo filter inversion (svg/png logos stay legible on both themes) */ +.fn-logo { + filter: var(--fn-logo-filter); + transition: filter var(--fn-duration-base) var(--fn-ease); +} + +/* Background-image icon sizing */ +.fn-icon { + background-size: round(100%, 0.5px) round(100%, 0.5px); +} diff --git a/id-sdk b/id-sdk new file mode 160000 index 0000000..6d0a8ef --- /dev/null +++ b/id-sdk @@ -0,0 +1 @@ +Subproject commit 6d0a8efbcc969e9cba6b080793d4b8c40da156c4 diff --git a/loop-sdk b/loop-sdk new file mode 160000 index 0000000..1a1309b --- /dev/null +++ b/loop-sdk @@ -0,0 +1 @@ +Subproject commit 1a1309b74d1aee27c8670d215feeca4b9c046d50 diff --git a/mkdocs.yml b/mkdocs.yml index 1d76cdb..56bafad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,22 @@ site_name: Operating Documents site_url: https://www.fivenorthgroup.com/ +site_description: "Foundational infrastructure documentation for decentralized financial networks — Five North Group." +site_author: "Five North Group" + +watch: + - overrides + extra_css: + - stylesheets/fn-tokens.css + - stylesheets/fn-layout.css - stylesheets/extra.css + +extra_javascript: + - javascripts/fn-theme.js + theme: name: material + custom_dir: overrides font: text: Inter Tight code: Red Hat Mono @@ -32,25 +45,10 @@ theme: - search.suggest - toc.follow - palette: - # Dark Mode - - media: "(prefers-color-scheme: light)" - scheme: slate - toggle: - icon: material/weather-sunny - name: Dark mode - primary: black - accent: black - - # Light Mode - - media: "(prefers-color-scheme: dark)" - scheme: default - toggle: - icon: material/weather-night - name: Light mode - primary: black - accent: black - + # Theme switching is handled by fn-theme.js (docs/javascripts/fn-theme.js). + # We deliberately DO NOT declare a `palette:` here so Material doesn't + # auto-apply a scheme on load or render its own palette switcher — that + # would fight the fivenorth theme toggle. plugins: - search @@ -86,8 +84,9 @@ markdown_extensions: copyright: Copyright © 2025 Five North Group nav: + - Home: index.md - 5N Infra: - - index.md + - 5n-infra/index.md - Hosted Node: hosted-node/hosted-node-access.md - Sandbox: sandbox/sandbox-access.md - Sponsor: diff --git a/overrides/home.html b/overrides/home.html new file mode 100644 index 0000000..753bcc3 --- /dev/null +++ b/overrides/home.html @@ -0,0 +1,141 @@ +{# + Home landing page template. + Ported from 5n-fivenorth-io/src/LandingPage.tsx (HeroSection + ProductsSection). + Used on any markdown page with `template: home.html` front-matter. +#} +{% extends "main.html" %} + +{# Product list — mirrors PRODUCTS array in site.ts. #} +{% set fn_products = [ + { + 'id': 'loop-sdk', + 'title': 'Loop SDK', + 'title_color': '#F2FF96', 'title_color_light': '#4D5A0A', + 'icon_bg': '#1E293B', + 'icon_url': 'assets/fn/icon-loop-sdk.svg', + 'icon_class': 'fn-card__icon--loop-sdk', + 'description': 'Loop SDK is a lightweight JavaScript client that allows dApps to securely connect to the Loop wallet. The SDK also includes a server-side signing flow for integrators who want to sign and submit transactions without a wallet popup.', + 'docs_url': 'loop-sdk/overview/' + }, + { + 'id': 'id-sdk', + 'title': 'ID SDK', + 'title_color': '#97E4FE', 'title_color_light': '#0369A1', + 'icon_bg': '#97E4FE', + 'icon_url': 'assets/fn/icon-id-sdk.svg', + 'icon_class': 'fn-card__icon--id-sdk', + 'description': 'The 5N ID service provides secure, tamper-proof identity verification and credential management through smart contracts on the Canton network.', + 'docs_url': 'id-sdk/introduction/' + }, + { + 'id': '5n-infra', + 'title': '5N Infra', + 'title_color': '#528BFF', 'title_color_light': '#1D4ED8', + 'icon_bg': '#042A77', + 'icon_url': 'assets/fn/icon-5n-infra.png', + 'icon_class': 'fn-card__icon--5n-infra', + 'is_img': true, + 'description': '5N provides hosted node service where we can operate operator for you. Contact nodesupport@fivenorth.io for detail.', + 'docs_url': '5n-infra/' + }, + { + 'id': '5n-dashboard', + 'title': '5N Dashboard', + 'title_color': '#E4DCF9', 'title_color_light': '#6D28D9', + 'icon_bg': '#E4DCF9', + 'icon_url': 'assets/fn/icon-5n-dashboard.svg', + 'icon_class': 'fn-card__icon--5n-dashboard', + 'description': 'The goal of the 5N Dashboard is to give developers and administrators the tools needed to effectively deploy, monitor, maintain, and back up both the node and their Canton‑based applications.', + 'docs_url': '5n-dashboard/introduction/' + }, + { + 'id': '5n-time-machine', + 'title': '5N Time Machine', + 'title_color': '#F0FCF0', 'title_color_light': '#15803D', + 'icon_bg': '#F0FCF0', + 'icon_url': 'assets/fn/icon-5n-time-machine.svg', + 'icon_class': 'fn-card__icon--5n-time-machine', + 'description': '5N Time Machine subscribes to your ledger, gets all the updates, and saves them into a Postgres database in a schema called time_machine.', + 'docs_url': '5n-timemachine/introduction/' + }, + { + 'id': 'coming-soon', + 'title': 'Coming Soon', + 'title_color': '#F8FAFC', 'title_color_light': '#374151', + 'icon_bg': '#161920', + 'icon_url': 'assets/fn/icon-coming-soon.svg', + 'icon_class': 'fn-card__icon--coming-soon', + 'description': 'More products are coming soon...' + } +] %} + +{% block container %} +
+
+
+
+ + {# ── Hero section ── #} +
+ + {# Grid mobile — shown above h1, hidden on desktop #} + + +
+

+ Foundational infrastructure for decentralized financial networks. +

+

+ At Five North Infrastructure, we're engineering the foundational + systems that empower next-generation financial networks: + Decentralized, privacy-preserving, and tailored for institutional scale. +

+
+ +
+ + {# ── Products grid ── #} +
+ {% for p in fn_products %} + {% if p.docs_url %} + + {% else %} + + {% endif %} + {% endfor %} +
+ +
+
+
+
+{% endblock %} diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..597b641 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,63 @@ +{# + Global template override for all MkDocs Material pages. + - Replaces the default Material header with the fivenorth header (includes pill tabs). + - Replaces the default Material footer with the fivenorth footer. + - Removes Material's announcement bar and default tabs row. +#} +{% extends "base.html" %} + +{# Apply theme synchronously in to prevent flash-of-wrong-theme. + Must stay in sync with docs/javascripts/fn-theme.js. #} +{% block extrahead %} + {{ super() }} + + {# ── Social / Open Graph meta ───────────────────────────────── #} + {% set _title = (page.title ~ ' — ' if page and page.title else '') ~ config.site_name %} + {% set _desc = page.meta.description if page and page.meta.description else config.site_description %} + {% set _url = page.canonical_url if page and page.canonical_url else config.site_url %} + {% set _img = config.site_url ~ 'assets/social.png' %} + + + + + + + + + + + + + + + + + + +{% endblock %} + +{% block announce %}{% endblock %} + +{% block header %} + {% include "partials/fn-header.html" %} +{% endblock %} + +{% block tabs %}{% endblock %} + +{% block footer %} + {% include "partials/fn-footer.html" %} +{% endblock %} diff --git a/overrides/partials/fn-footer.html b/overrides/partials/fn-footer.html new file mode 100644 index 0000000..dd03b3f --- /dev/null +++ b/overrides/partials/fn-footer.html @@ -0,0 +1,62 @@ +{# + Fivenorth custom footer. + Ported from 5n-fivenorth-io/src/sections/Footer.tsx. +#} +{% set fn_footer_links = [ + { 'label': 'About', 'url': 'https://www.fivenorthdigital.com/' }, + { 'label': 'Contact', 'url': 'https://www.fivenorthdigital.com/for-developers' } +] %} + + diff --git a/overrides/partials/fn-header.html b/overrides/partials/fn-header.html new file mode 100644 index 0000000..07b6ac4 --- /dev/null +++ b/overrides/partials/fn-header.html @@ -0,0 +1,94 @@ +{# + Fivenorth custom header — FOUR sibling elements at body level: + + 1.
— logo + title, scrolls away. + 2.
— tabs sticky bar. + 3.
— search component (sibling, NOT inside + .fn-header__sticky which has backdrop- + filter; placing it here lets position:fixed + work relative to the real viewport). + 4.
— hamburger drawer. +#} +{% set fn_tabs = [ + { 'label': 'Home', 'href': '' | url }, + { 'label': '5N Infra', 'href': '5n-infra/' | url }, + { 'label': '5N Dashboard', 'href': '5n-dashboard/introduction/' | url }, + { 'label': 'Loop SDK', 'href': 'loop-sdk/overview/' | url }, + { 'label': 'ID SDK', 'href': 'id-sdk/introduction/' | url }, + { 'label': '5N Time Machine', 'href': '5n-timemachine/introduction/' | url } +] %} + +{# ── 1. Brand bar (scrolls with page) ────────────────────────── #} + + +{# ── 2. Sticky action bar ───────────────────────────────────────── + MUST be a
tag: Material's search worker calls + R("header [for=__search]") on init. We satisfy it with a hidden stub + label so the real .fn-search-wrap can live OUTSIDE this element. #} + + +{# ── 3. Search wrap — sibling of .fn-header__sticky ────────────── + The toggle checkbox `#__search` is emitted automatically by Material's + base.html at body level (before this header), so the CSS sibling + selector `[data-md-toggle="search"]:checked ~ .fn-search-wrap ...` + still works — no need for our own duplicate toggle. + + No backdrop-filter ancestor → position:fixed works vs viewport. + Desktop: fixed, right-aligned, same height as sticky bar. + Mobile: fixed full-screen overlay when fn-mobile-search-active. #} +
+ {% include "partials/search.html" %} +
+ +{# ── 5. Mobile nav drawer ─────────────────────────────────────── #} +
+
+
+ {% for tab in fn_tabs %} + + {{ tab.label }} + + {% endfor %} +
+
+