Split repo into noo and portfolio workers
Deploy noo (noo.ayushya.dev) / deploy (push) Successful in 26s
Deploy portfolio (ayushya.dev) / deploy (push) Successful in 15s

- noo: moved from nextcloud_client/website, deploys to noo.ayushya.dev
- portfolio: moved root site into portfolio/, deploys to ayushya.dev
- separate path-filtered Gitea workflows per worker
This commit is contained in:
2026-10-01 14:01:12 +00:00
parent 35566e0c3d
commit 68ba560c15
14 changed files with 1727 additions and 425 deletions
@@ -1,9 +1,12 @@
name: Deploy to Cloudflare Workers name: Deploy noo (noo.ayushya.dev)
on: on:
push: push:
branches: branches:
- main # change if your default branch is named differently, e.g. master - main
paths:
- "noo/**"
- ".gitea/workflows/deploy-noo.yml"
jobs: jobs:
deploy: deploy:
@@ -21,3 +24,4 @@ jobs:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "4" wranglerVersion: "4"
workingDirectory: noo
+27
View File
@@ -0,0 +1,27 @@
name: Deploy portfolio (ayushya.dev)
on:
push:
branches:
- main
paths:
- "portfolio/**"
- ".gitea/workflows/deploy-portfolio.yml"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# Absolute https:// URLs are used here (rather than the bare
# `actions/checkout@v4` shorthand) so this works regardless of how this
# Gitea instance has DEFAULT_ACTIONS_URL configured.
- name: Checkout
uses: https://github.com/actions/checkout@v4
- name: Deploy
uses: https://github.com/cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "4"
workingDirectory: portfolio
+5
View File
@@ -0,0 +1,5 @@
wrangler.toml
README.md
.assetsignore
node_modules
.wrangler
+38
View File
@@ -0,0 +1,38 @@
# Noo website
The product site for Noo, meant to be deployed at `noo.ayushya.dev`.
Static HTML/CSS/vanilla JS, no build step and no dependencies beyond the
two Google Fonts already used by the app (Schibsted Grotesk, Instrument
Sans). Colors, radii, type scale and motion timing are copied from
`lib/theme/design_tokens.dart` so the site and the app read as the same
product.
## Files
- `index.html` — the whole page (hero, features, customize, privacy, CTA).
- `styles.css` — all styling, incl. the design tokens as CSS custom
properties and a dark-mode variant (`prefers-color-scheme` and an
explicit `data-theme` override for a future toggle).
- `script.js` — mobile nav, the hero's exploded-layer parallax
(scroll + pointer), the two interactive Customize demos (accent color,
bottom bar style), and scroll-reveal.
- `favicon.svg`.
## Local preview
Any static file server works, e.g.:
```bash
cd website
python3 -m http.server 8000
```
Then open `http://localhost:8000`.
## Deploying
Point any static host (Cloudflare Pages, Netlify, GitHub Pages, or a
plain nginx `root`) at this folder - there's nothing to build. Update the
`https://github.com` placeholder links and the two "Download for..."
buttons in `index.html` once real release links exist.
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="9" fill="#8D0DE3"/>
<rect x="9" y="9" width="14" height="14" rx="5" fill="#FFFFFF" fill-opacity="0.92"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

+307
View File
@@ -0,0 +1,307 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Noo — a Nextcloud client that feels like it belongs on your phone</title>
<meta name="description" content="Noo is a fast, private, deeply customizable Nextcloud client for Android and iOS. Browse files and photos, sync for offline, and make it look like yours.">
<meta property="og:title" content="Noo — a Nextcloud client that feels like it belongs on your phone">
<meta property="og:description" content="Fast, private, and deeply customizable. Browse files and photos, sync for offline, and make Noo look like yours.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://noo.ayushya.dev">
<meta name="theme-color" content="#F3EFE6">
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600&family=Schibsted+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="wrap header-row">
<a class="wordmark" href="#top">
<span class="wordmark-mark" aria-hidden="true"></span>
noo
</a>
<nav class="site-nav" aria-label="Primary">
<a href="#features">Features</a>
<a href="#customize">Customize</a>
<a href="#privacy">Privacy</a>
</nav>
<a class="btn btn-primary btn-sm" href="#get">Get Noo</a>
<button class="nav-toggle" id="navToggle" aria-label="Open menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
</div>
<nav class="mobile-nav" id="mobileNav" aria-label="Primary">
<a href="#features">Features</a>
<a href="#customize">Customize</a>
<a href="#privacy">Privacy</a>
<a class="btn btn-primary btn-sm" href="#get">Get Noo</a>
</nav>
</header>
<main id="main">
<!-- ============ HERO ============ -->
<section class="hero" id="top">
<div class="wrap hero-grid">
<div class="hero-copy">
<p class="eyebrow">For Nextcloud, unofficial</p>
<h1>Your cloud.<br>Looking like <em>yours</em>.</h1>
<p class="hero-sub">
Noo is a fast, private Nextcloud client that browses files and
photos the way a native app should — and lets you rebuild its
whole shell around how you actually use your phone.
</p>
<div class="hero-actions">
<a class="btn btn-primary btn-lg" href="#get">Get Noo</a>
<a class="btn btn-outline btn-lg" href="https://github.com" target="_blank" rel="noopener">View source</a>
</div>
<ul class="hero-proof">
<li>No password stored — Login Flow v2</li>
<li>Offline device sync</li>
<li>Open source</li>
</ul>
</div>
<div class="hero-visual" id="heroVisual" aria-hidden="true">
<div class="glow"></div>
<div class="phone" data-depth="0.15">
<div class="phone-notch"></div>
<div class="phone-screen">
<div class="mini-topbar">
<span class="mini-title">Files</span>
<span class="mini-dot"></span>
</div>
<div class="mini-list">
<div class="mini-row"><span class="mini-ic ic-folder"></span><span class="mini-lines"><b></b><i></i></span></div>
<div class="mini-row"><span class="mini-ic ic-image"></span><span class="mini-lines"><b></b><i></i></span></div>
<div class="mini-row mini-row-active"><span class="mini-ic ic-file"></span><span class="mini-lines"><b></b><i></i></span></div>
<div class="mini-row"><span class="mini-ic ic-folder"></span><span class="mini-lines"><b></b><i></i></span></div>
</div>
<div class="mini-bar">
<span class="mini-bar-pill"></span>
<span class="mini-bar-dot"></span>
<span class="mini-bar-dot"></span>
<span class="mini-bar-dot"></span>
<span class="mini-bar-dot"></span>
</div>
</div>
</div>
<div class="chip chip-photo" data-depth="0.55">
<div class="chip-photo-grid">
<span></span><span></span><span></span><span></span>
</div>
</div>
<div class="chip chip-row" data-depth="0.4">
<span class="mini-ic ic-share"></span>
<span class="mini-lines"><b></b><i></i></span>
</div>
<div class="chip chip-swatches" data-depth="0.7">
<span class="swatch" style="--c:#8D0DE3"></span>
<span class="swatch" style="--c:#2940A8"></span>
<span class="swatch" style="--c:#3E7A23"></span>
<span class="swatch" style="--c:#8C5A0E"></span>
</div>
<div class="chip chip-toggle" data-depth="0.5">
<span class="mini-lines mini-lines-toggle"><b></b><i></i></span>
<span class="mini-toggle mini-toggle-on"><i></i></span>
</div>
<div class="chip chip-badge" data-depth="0.85">
<span class="mini-ic ic-lock"></span>
Offline ready
</div>
</div>
</div>
</section>
<!-- ============ FEATURES ============ -->
<section class="section" id="features">
<div class="wrap">
<p class="eyebrow">Features</p>
<h2>Everything your Nextcloud does. Nothing it doesn't.</h2>
<p class="section-sub">
Files, photos, sharing and sync — built as a real mobile app, not a
browser tab pinned to your home screen.
</p>
<div class="grid grid-features">
<article class="card">
<span class="card-ic ic-folder-lg"></span>
<h3>Files, browsed properly</h3>
<p>Grid or list, sorted your way, with the same filters and
controls whether you're picking a file, uploading, or moving
one into place.</p>
</article>
<article class="card">
<span class="card-ic ic-image-lg"></span>
<h3>Photos, grouped by month</h3>
<p>Every image and video across your account in one gallery —
filter to just photos, just videos, or just favorites.</p>
</article>
<article class="card">
<span class="card-ic ic-download-lg"></span>
<h3>Offline, for real</h3>
<p>Pick folders to mirror locally and keep browsing — reading,
opening, previewing — with no connection at all.</p>
</article>
<article class="card">
<span class="card-ic ic-share-lg"></span>
<h3>Sharing, without the detour</h3>
<p>Share with people or a public link, see who has access, and
copy that link — all from one sheet, not three menus.</p>
</article>
<article class="card">
<span class="card-ic ic-clock-lg"></span>
<h3>Activity &amp; recent</h3>
<p>What changed, and where — grouped into today, yesterday and
this week, so catching up takes seconds.</p>
</article>
<article class="card">
<span class="card-ic ic-users-lg"></span>
<h3>Multiple accounts</h3>
<p>Switch between personal and work Nextcloud instances with a
swipe — each with its own sync, cache and lock.</p>
</article>
</div>
</div>
</section>
<!-- ============ CUSTOMIZE ============ -->
<section class="section section-alt" id="customize">
<div class="wrap">
<p class="eyebrow">Customize</p>
<h2>Shape it around your thumb, not the other way round.</h2>
<p class="section-sub">
Every screen bends to a handful of real settings — try a few
right here.
</p>
<div class="customize-grid">
<div class="customize-demo" id="accentDemo">
<div class="demo-preview">
<div class="demo-topbar">
<span class="demo-dot" style="background:var(--demo-accent)"></span>
<span class="demo-line" style="width:38%"></span>
</div>
<div class="demo-pill" style="background:var(--surface3); background:color-mix(in srgb, var(--demo-accent) 16%, transparent); color:var(--demo-accent)">Active tab</div>
<div class="demo-row"><span class="demo-line" style="width:60%"></span></div>
<div class="demo-row"><span class="demo-line" style="width:44%"></span></div>
</div>
<div class="demo-controls">
<h3>Pick your accent</h3>
<p>Six built-in colors, or match your wallpaper with dynamic
color on Android.</p>
<div class="swatch-row" role="group" aria-label="Accent color">
<button class="swatch-btn is-active" style="--c:#8D0DE3" data-accent="#8D0DE3" aria-label="Violet"></button>
<button class="swatch-btn" style="--c:#2940A8" data-accent="#2940A8" aria-label="Blue"></button>
<button class="swatch-btn" style="--c:#3E7A23" data-accent="#3E7A23" aria-label="Green"></button>
<button class="swatch-btn" style="--c:#8C5A0E" data-accent="#8C5A0E" aria-label="Amber"></button>
<button class="swatch-btn" style="--c:#A8202A" data-accent="#A8202A" aria-label="Red"></button>
<button class="swatch-btn" style="--c:#0E0D0B" data-accent="#0E0D0B" aria-label="Wallpaper"></button>
</div>
</div>
</div>
<div class="customize-demo" id="barDemo">
<div class="demo-preview demo-preview-bar">
<div class="demo-screen-fill"></div>
<div class="demo-bar" id="demoBar">
<span class="demo-bar-item is-on"></span>
<span class="demo-bar-item"></span>
<span class="demo-bar-item"></span>
<span class="demo-bar-item"></span>
</div>
</div>
<div class="demo-controls">
<h3>Floating or attached</h3>
<p>The bottom bar can sit flush with the edge, or float free
with rounded corners — your call.</p>
<div class="segmented" role="group" aria-label="Bottom bar style">
<button class="segmented-btn" data-bar="attached">Attached</button>
<button class="segmented-btn is-active" data-bar="floating">Floating</button>
</div>
</div>
</div>
<div class="customize-list">
<h3>And the rest</h3>
<ul class="feature-list">
<li>Reorder tabs, hide the ones you never open</li>
<li>Move Search into the bottom bar</li>
<li>Light, dark, or match the system</li>
<li>Pure black backgrounds in dark mode</li>
<li>Choose what swiping a file left or right does</li>
<li>Reorder the bulk-action bar for multi-select</li>
</ul>
</div>
</div>
</div>
</section>
<!-- ============ PRIVACY ============ -->
<section class="section" id="privacy">
<div class="wrap privacy-grid">
<div>
<p class="eyebrow">Privacy</p>
<h2>It's your server. Noo just visits.</h2>
<p class="section-sub">
Noo never asks for your password. Signing in hands off to your
Nextcloud server's own login page (Login Flow v2), which returns
a revocable app token — not your credentials. Nothing but that
token, and whatever you choose to sync, ever leaves your device.
</p>
</div>
<ul class="privacy-points">
<li><span class="pt-ic ic-lock"></span> No password stored on device</li>
<li><span class="pt-ic ic-server"></span> Talks only to your own server</li>
<li><span class="pt-ic ic-code"></span> Open source, auditable</li>
<li><span class="pt-ic ic-off"></span> No analytics, no trackers</li>
</ul>
</div>
</section>
<!-- ============ GET ============ -->
<section class="section section-cta" id="get">
<div class="wrap cta-inner">
<h2>Bring your Nextcloud with you.</h2>
<p class="section-sub">Free and open source. Point it at your server and go.</p>
<div class="hero-actions cta-actions">
<a class="btn btn-primary btn-lg" href="#">Download for Android</a>
<a class="btn btn-outline btn-lg" href="#">Download for iOS</a>
</div>
<p class="cta-fine">Or <a href="https://github.com" target="_blank" rel="noopener">build it from source</a> — it's just Flutter.</p>
</div>
</section>
</main>
<footer class="site-footer">
<div class="wrap footer-row">
<a class="wordmark wordmark-sm" href="#top">
<span class="wordmark-mark" aria-hidden="true"></span>
noo
</a>
<p class="footer-note">Noo is an independent, unofficial Nextcloud client. Nextcloud is a trademark of Nextcloud GmbH.</p>
<nav class="footer-links" aria-label="Footer">
<a href="https://github.com" target="_blank" rel="noopener">GitHub</a>
<a href="#privacy">Privacy</a>
</nav>
</div>
</footer>
<script src="script.js"></script>
</body>
</html>
+141
View File
@@ -0,0 +1,141 @@
(() => {
"use strict";
/* ---------- mobile nav ---------- */
const navToggle = document.getElementById("navToggle");
const mobileNav = document.getElementById("mobileNav");
if (navToggle && mobileNav) {
navToggle.addEventListener("click", () => {
const open = mobileNav.classList.toggle("is-open");
navToggle.setAttribute("aria-expanded", String(open));
navToggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
});
mobileNav.querySelectorAll("a").forEach((a) =>
a.addEventListener("click", () => {
mobileNav.classList.remove("is-open");
navToggle.setAttribute("aria-expanded", "false");
})
);
}
/* ---------- exploded hero parallax ---------- */
/* Each [data-depth] layer drifts at its own rate on scroll, and tilts
gently toward the pointer - the "exploded" pieces read as separate
depths rather than a flat collage. Skipped entirely under
prefers-reduced-motion. */
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const heroVisual = document.getElementById("heroVisual");
if (heroVisual && !reduceMotion) {
const layers = Array.from(heroVisual.querySelectorAll("[data-depth]")).map((el) => ({
el,
depth: parseFloat(el.dataset.depth) || 0.3,
}));
let scrollOffset = 0;
let pointer = { x: 0, y: 0 }; // -1..1
let ticking = false;
const apply = () => {
const rect = heroVisual.getBoundingClientRect();
const inView = rect.bottom > 0 && rect.top < window.innerHeight;
if (inView) {
for (const layer of layers) {
const scrollShift = scrollOffset * layer.depth * 40;
const px = pointer.x * layer.depth * 14;
const py = pointer.y * layer.depth * 14;
layer.el.style.transform =
`translate3d(${px}px, ${scrollShift + py}px, 0)`;
}
}
ticking = false;
};
const requestTick = () => {
if (!ticking) {
ticking = true;
requestAnimationFrame(apply);
}
};
const onScroll = () => {
const rect = heroVisual.getBoundingClientRect();
// -1 (scrolled well past) .. 1 (below viewport) - 0 while centered.
scrollOffset = (window.innerHeight / 2 - (rect.top + rect.height / 2)) / window.innerHeight;
requestTick();
};
const onPointerMove = (e) => {
const rect = heroVisual.getBoundingClientRect();
pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = ((e.clientY - rect.top) / rect.height) * 2 - 1;
requestTick();
};
const onPointerLeave = () => {
pointer = { x: 0, y: 0 };
requestTick();
};
window.addEventListener("scroll", onScroll, { passive: true });
heroVisual.addEventListener("pointermove", onPointerMove);
heroVisual.addEventListener("pointerleave", onPointerLeave);
onScroll();
}
/* ---------- accent color demo ---------- */
const accentDemo = document.getElementById("accentDemo");
if (accentDemo) {
const buttons = accentDemo.querySelectorAll(".swatch-btn");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
buttons.forEach((b) => b.classList.remove("is-active"));
btn.classList.add("is-active");
const color = btn.dataset.accent;
accentDemo.style.setProperty("--demo-accent", color);
});
});
}
/* ---------- bottom bar style demo ---------- */
const barDemo = document.getElementById("barDemo");
const demoBar = document.getElementById("demoBar");
if (barDemo && demoBar) {
const buttons = barDemo.querySelectorAll(".segmented-btn");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
buttons.forEach((b) => b.classList.remove("is-active"));
btn.classList.add("is-active");
demoBar.classList.toggle("is-attached", btn.dataset.bar === "attached");
});
});
}
/* ---------- scroll reveal ---------- */
const revealTargets = document.querySelectorAll(
".card, .customize-demo, .customize-list, .privacy-points li"
);
revealTargets.forEach((el) => el.classList.add("reveal"));
if ("IntersectionObserver" in window && !reduceMotion) {
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
}
},
{ threshold: 0.15, rootMargin: "0px 0px -40px 0px" }
);
revealTargets.forEach((el) => io.observe(el));
} else {
revealTargets.forEach((el) => el.classList.add("is-visible"));
}
})();
+758
View File
@@ -0,0 +1,758 @@
/* Noo marketing site — tokens mirror lib/theme/design_tokens.dart exactly,
so this reads as the same product, not a separate marketing skin. */
:root {
--bg: #F3EFE6;
--surface: #FFFFFF;
--surface2: #F6F5F2;
--surface3: #EAE8E2;
--line: #EAE8E2;
--fg1: #0E0D0B;
--fg2: #5A5852;
--fg3: #7C7A72;
--accent: #8D0DE3;
--accent-text: #8D0DE3;
--accent-soft: #ECDFFF;
--danger: #A8202A;
--demo-accent: #8D0DE3;
--font-display: 'Schibsted Grotesk', system-ui, sans-serif;
--font-body: 'Instrument Sans', system-ui, sans-serif;
--ease: cubic-bezier(0.7, 0, 0.15, 1);
--motion-fast: 180ms;
--motion-base: 280ms;
--radius-pill: 999px;
--radius-card: 20px;
--radius-input: 14px;
--radius-panel: 28px;
--gutter: clamp(20px, 5vw, 64px);
--content-max: 1120px;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0E0D0B;
--surface: #25241F;
--surface2: #3D3C38;
--surface3: #5A5852;
--line: #3D3C38;
--fg1: #F3EFE6;
--fg2: #BDBAB1;
--fg3: #9E9B92;
--accent: #8D0DE3;
--accent-text: #CBA5FD;
--accent-soft: #2F0454;
--danger: #F49E9E;
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--bg: #0E0D0B;
--surface: #25241F;
--surface2: #3D3C38;
--surface3: #5A5852;
--line: #3D3C38;
--fg1: #F3EFE6;
--fg2: #BDBAB1;
--fg3: #9E9B92;
--accent: #8D0DE3;
--accent-text: #CBA5FD;
--accent-soft: #2F0454;
--danger: #F49E9E;
color-scheme: dark;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--fg1);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, svg { display: block; max-width: 100%; }
a { color: inherit; }
h1, h2, h3 {
font-family: var(--font-display);
font-weight: 600;
margin: 0;
letter-spacing: -0.02em;
}
.wrap {
max-width: var(--content-max);
margin: 0 auto;
padding-inline: var(--gutter);
}
.skip-link {
position: absolute;
left: -9999px;
top: 0;
background: var(--accent);
color: #fff;
padding: 10px 16px;
border-radius: var(--radius-input);
z-index: 100;
}
.skip-link:focus { left: 16px; top: 16px; }
.eyebrow {
font-family: var(--font-body);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent-text);
margin: 0 0 12px;
}
/* ---------- buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: var(--font-body);
font-weight: 600;
text-decoration: none;
border-radius: var(--radius-pill);
border: 1px solid transparent;
cursor: pointer;
transition: transform var(--motion-fast) var(--ease), opacity var(--motion-fast) var(--ease), background var(--motion-fast) var(--ease);
white-space: nowrap;
}
.btn:active { transform: scale(0.98); }
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { opacity: 0.92; }
.btn-outline { background: transparent; color: var(--fg1); border-color: var(--line); }
.btn-outline:hover { background: var(--surface2); }
.btn-lg { padding: 14px 26px; font-size: 16px; }
.btn-sm { padding: 9px 18px; font-size: 14px; }
/* ---------- header ---------- */
.site-header {
position: sticky;
top: 0;
z-index: 40;
background: var(--bg);
border-bottom: 1px solid var(--line);
}
.header-row {
display: flex;
align-items: center;
gap: 24px;
height: 72px;
}
.wordmark {
display: flex;
align-items: center;
gap: 10px;
font-family: var(--font-display);
font-weight: 700;
font-size: 20px;
text-decoration: none;
color: var(--fg1);
margin-right: auto;
}
.wordmark-sm { font-size: 17px; }
.wordmark-mark {
width: 26px;
height: 26px;
border-radius: 8px;
background: var(--accent);
position: relative;
flex: none;
}
.wordmark-mark::after {
content: "";
position: absolute;
inset: 7px;
border-radius: 4px;
background: #fff;
opacity: 0.92;
}
.site-nav {
display: flex;
gap: 28px;
}
.site-nav a {
font-size: 15px;
font-weight: 500;
text-decoration: none;
color: var(--fg2);
transition: color var(--motion-fast) var(--ease);
}
.site-nav a:hover { color: var(--fg1); }
.nav-toggle {
display: none;
flex-direction: column;
justify-content: center;
gap: 4px;
width: 40px;
height: 40px;
border: none;
background: none;
cursor: pointer;
margin-left: -6px;
}
.nav-toggle span {
display: block;
height: 2px;
border-radius: 2px;
background: var(--fg1);
transition: transform var(--motion-base) var(--ease), opacity var(--motion-base) var(--ease);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.mobile-nav {
display: none;
flex-direction: column;
gap: 4px;
padding: 8px var(--gutter) 20px;
border-top: 1px solid var(--line);
}
.mobile-nav a {
padding: 12px 4px;
text-decoration: none;
color: var(--fg1);
font-weight: 500;
border-bottom: 1px solid var(--line);
}
.mobile-nav a:last-child { border-bottom: none; margin-top: 8px; text-align: center; }
.mobile-nav.is-open { display: flex; }
@media (max-width: 760px) {
.site-nav, .header-row > .btn { display: none; }
.nav-toggle { display: flex; }
}
/* ---------- hero ---------- */
.hero { padding: clamp(48px, 8vw, 96px) 0 clamp(40px, 6vw, 72px); overflow: clip; }
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: clamp(32px, 6vw, 64px);
align-items: center;
}
.hero-copy h1 {
font-size: clamp(38px, 5.2vw, 60px);
line-height: 1.02;
margin-bottom: 20px;
}
.hero-copy h1 em {
font-style: normal;
color: var(--accent-text);
}
.hero-sub {
font-size: clamp(16px, 1.6vw, 18px);
color: var(--fg2);
max-width: 46ch;
margin: 0 0 28px;
}
.hero-actions { display: flex; gap: 14px; flex-wrap: wrap; }
.hero-proof {
list-style: none;
margin: 28px 0 0;
padding: 0;
display: flex;
gap: 20px;
flex-wrap: wrap;
font-size: 13px;
color: var(--fg3);
}
.hero-proof li { display: flex; align-items: center; gap: 7px; }
.hero-proof li::before {
content: "";
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent);
}
/* ---------- hero exploded visual ---------- */
.hero-visual {
position: relative;
height: clamp(360px, 46vw, 520px);
display: flex;
align-items: center;
justify-content: center;
}
.glow {
position: absolute;
inset: -10%;
background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%);
pointer-events: none;
}
.phone {
position: relative;
width: clamp(180px, 19vw, 220px);
aspect-ratio: 9 / 18.5;
background: var(--fg1);
border-radius: 34px;
padding: 10px;
will-change: transform;
}
.phone-notch {
position: absolute;
top: 10px; left: 50%;
transform: translateX(-50%);
width: 34%; height: 16px;
background: var(--fg1);
border-radius: 0 0 12px 12px;
z-index: 2;
}
.phone-screen {
position: relative;
height: 100%;
background: var(--bg);
border-radius: 24px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mini-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22px 14px 8px;
}
.mini-title { font-family: var(--font-display); font-weight: 600; font-size: 15px; color: var(--fg1); }
.mini-dot { width: 16px; height: 16px; border-radius: 50%; background: var(--surface2); }
.mini-list { flex: 1; padding: 4px 10px; display: flex; flex-direction: column; gap: 6px; }
.mini-row {
display: flex;
align-items: center;
gap: 8px;
background: var(--surface);
border-radius: 10px;
padding: 7px 8px;
}
.mini-row-active { background: var(--accent-soft); }
.mini-ic { width: 16px; height: 16px; border-radius: 5px; flex: none; }
.ic-folder { background: var(--accent); opacity: 0.85; }
.ic-image { background: #2940A8; opacity: 0.8; }
.ic-file { background: var(--fg2); opacity: 0.6; }
.ic-share { background: #3E7A23; opacity: 0.85; }
.ic-lock { background: var(--fg1); opacity: 0.9; }
.mini-lines { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; }
.mini-lines b, .mini-lines i {
display: block;
height: 5px;
border-radius: 3px;
background: var(--fg2);
opacity: 0.35;
font-style: normal;
}
.mini-lines b { width: 70%; opacity: 0.55; }
.mini-lines i { width: 40%; }
.mini-bar {
display: flex;
align-items: center;
justify-content: space-around;
padding: 10px 8px;
border-top: 1px solid var(--line);
position: relative;
}
.mini-bar-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--fg3); opacity: 0.4; }
.mini-bar-pill {
position: absolute;
left: 10px;
width: 20px; height: 10px;
border-radius: var(--radius-pill);
background: var(--accent-soft);
}
.chip {
position: absolute;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-card);
will-change: transform;
}
.chip-photo {
top: 6%; right: 2%;
width: 92px; height: 92px;
padding: 8px;
transform: rotate(-6deg);
}
.chip-photo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; height: 100%; }
.chip-photo-grid span:nth-child(1) { background: #8DCDE2; border-radius: 8px; }
.chip-photo-grid span:nth-child(2) { background: #E5D8BD; border-radius: 8px; }
.chip-photo-grid span:nth-child(3) { background: #DA9AC7; border-radius: 8px; }
.chip-photo-grid span:nth-child(4) { background: #A7D296; border-radius: 8px; }
.chip-row {
bottom: 20%; right: -2%;
display: flex; align-items: center; gap: 10px;
padding: 12px 16px;
transform: rotate(4deg);
width: 160px;
}
.chip-swatches {
bottom: 6%; left: 4%;
display: flex; gap: 8px;
padding: 12px;
transform: rotate(-4deg);
}
.chip-swatches .swatch { width: 18px; height: 18px; border-radius: 50%; background: var(--c); }
.chip-toggle {
top: 16%; left: -4%;
display: flex; align-items: center; gap: 14px;
padding: 12px 14px;
width: 150px;
transform: rotate(3deg);
}
.mini-lines-toggle { flex: 1; }
.mini-toggle { width: 30px; height: 18px; border-radius: var(--radius-pill); background: var(--surface3); position: relative; flex: none; }
.mini-toggle i { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--fg1); opacity: 0.3; }
.mini-toggle-on { background: var(--accent); }
.mini-toggle-on i { left: 14px; background: #fff; opacity: 1; }
.chip-badge {
bottom: -2%; left: 30%;
display: flex; align-items: center; gap: 8px;
padding: 9px 14px;
font-size: 13px;
font-weight: 500;
border-radius: var(--radius-pill);
transform: rotate(-2deg);
}
.chip-badge .mini-ic { width: 14px; height: 14px; }
@media (max-width: 980px) {
.hero-grid { grid-template-columns: 1fr; text-align: center; }
.hero-copy { order: 2; }
.hero-copy .hero-sub { margin-inline: auto; }
.hero-actions, .hero-proof { justify-content: center; }
.hero-visual { order: 1; }
}
/* ---------- sections ---------- */
.section { padding: clamp(56px, 8vw, 104px) 0; }
.section-alt { background: var(--surface); }
.section h2 {
font-size: clamp(28px, 3.4vw, 42px);
line-height: 1.08;
max-width: 20ch;
}
.section-sub {
font-size: clamp(15px, 1.4vw, 17px);
color: var(--fg2);
max-width: 56ch;
margin: 16px 0 0;
}
.grid { display: grid; gap: 20px; margin-top: 44px; }
.grid-features { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.card {
background: var(--surface);
border-radius: var(--radius-card);
padding: 28px;
}
.section-alt .card { background: var(--bg); }
.card h3 {
font-size: 19px;
margin: 16px 0 8px;
}
.card p { margin: 0; color: var(--fg2); font-size: 15px; }
.card-ic, .pt-ic {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px; height: 44px;
border-radius: var(--radius-input);
background: var(--accent-soft);
position: relative;
}
.card-ic::after, .pt-ic::after {
content: "";
width: 20px; height: 20px;
background: var(--accent-text);
-webkit-mask-size: contain;
mask-size: contain;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
}
/* Minimal inline icon set (stroke-style, Lucide-ish) via CSS mask so the
whole page stays dependency-free - no icon font, no SVG sprite fetch. */
.ic-folder-lg::after { -webkit-mask-image: var(--i-folder); mask-image: var(--i-folder); }
.ic-image-lg::after { -webkit-mask-image: var(--i-image); mask-image: var(--i-image); }
.ic-download-lg::after { -webkit-mask-image: var(--i-download); mask-image: var(--i-download); }
.ic-share-lg::after { -webkit-mask-image: var(--i-share); mask-image: var(--i-share); }
.ic-clock-lg::after { -webkit-mask-image: var(--i-clock); mask-image: var(--i-clock); }
.ic-users-lg::after { -webkit-mask-image: var(--i-users); mask-image: var(--i-users); }
.pt-ic.ic-lock::after { -webkit-mask-image: var(--i-lock); mask-image: var(--i-lock); }
.pt-ic.ic-server::after { -webkit-mask-image: var(--i-server); mask-image: var(--i-server); }
.pt-ic.ic-code::after { -webkit-mask-image: var(--i-code); mask-image: var(--i-code); }
.pt-ic.ic-off::after { -webkit-mask-image: var(--i-off); mask-image: var(--i-off); }
:root {
--i-folder: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z'/%3E%3C/svg%3E");
--i-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='18' x='3' y='3' rx='2'/%3E%3Ccircle cx='9' cy='9' r='2'/%3E%3Cpath d='m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21'/%3E%3C/svg%3E");
--i-download: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3Cpolyline points='7 10 12 15 17 10'/%3E%3Cline x1='12' y1='15' x2='12' y2='3'/%3E%3C/svg%3E");
--i-share: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'/%3E%3Ccircle cx='6' cy='12' r='3'/%3E%3Ccircle cx='18' cy='19' r='3'/%3E%3Cline x1='8.6' y1='10.5' x2='15.4' y2='6.5'/%3E%3Cline x1='8.6' y1='13.5' x2='15.4' y2='17.5'/%3E%3C/svg%3E");
--i-clock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpolyline points='12 7 12 12 15.5 14.5'/%3E%3C/svg%3E");
--i-users: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='9' cy='7' r='4'/%3E%3Cpath d='M22 21v-2a4 4 0 0 0-3-3.87'/%3E%3Cpath d='M16 3.13a4 4 0 0 1 0 7.75'/%3E%3C/svg%3E");
--i-lock: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E");
--i-server: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3' width='20' height='7' rx='1.5'/%3E%3Crect x='2' y='14' width='20' height='7' rx='1.5'/%3E%3Cline x1='6' y1='6.5' x2='6.01' y2='6.5'/%3E%3Cline x1='6' y1='17.5' x2='6.01' y2='17.5'/%3E%3C/svg%3E");
--i-code: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='16 18 22 12 16 6'/%3E%3Cpolyline points='8 6 2 12 8 18'/%3E%3C/svg%3E");
--i-off: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='2' y1='2' x2='22' y2='22'/%3E%3Cpath d='M16.9 16.9A9 9 0 0 1 4 12a9 9 0 0 1 2.6-6.3'/%3E%3Cpath d='M9.9 4.24A9 9 0 0 1 20 12a9 9 0 0 1-.8 3.7'/%3E%3C/svg%3E");
}
@media (max-width: 900px) { .grid-features { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 600px) { .grid-features { grid-template-columns: 1fr; } }
/* ---------- customize ---------- */
.customize-grid {
margin-top: 44px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
.customize-demo {
background: var(--bg);
border-radius: var(--radius-card);
padding: 24px;
display: flex;
flex-direction: column;
gap: 22px;
}
.customize-demo h3 { font-size: 18px; margin: 0 0 8px; }
.customize-demo p { margin: 0; color: var(--fg2); font-size: 14px; }
.demo-preview {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-input);
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.demo-topbar { display: flex; align-items: center; gap: 10px; }
.demo-dot { width: 12px; height: 12px; border-radius: 50%; transition: background var(--motion-base) var(--ease); }
.demo-line { display: block; height: 8px; border-radius: 4px; background: var(--surface3); }
.demo-pill {
align-self: flex-start;
padding: 6px 14px;
border-radius: var(--radius-pill);
font-size: 13px;
font-weight: 600;
transition: background var(--motion-base) var(--ease), color var(--motion-base) var(--ease);
}
.demo-row { padding: 6px 0; }
.swatch-row { display: flex; gap: 10px; }
.swatch-btn {
width: 32px; height: 32px;
border-radius: 50%;
border: 2px solid transparent;
background: var(--c);
cursor: pointer;
padding: 0;
transition: transform var(--motion-fast) var(--ease), border-color var(--motion-fast) var(--ease);
}
.swatch-btn:last-child {
background: linear-gradient(135deg, #8DCDE2, #DA9AC7 50%, #E5D8BD);
}
.swatch-btn.is-active { border-color: var(--fg1); transform: scale(1.08); }
.swatch-btn:hover { transform: scale(1.08); }
.demo-preview-bar { position: relative; height: 128px; padding: 0; overflow: hidden; }
.demo-screen-fill {
position: absolute; inset: 0;
background: repeating-linear-gradient(180deg, var(--surface2) 0 34px, var(--surface) 34px 68px);
opacity: 0.6;
}
.demo-bar {
position: absolute;
left: 12px; right: 12px; bottom: 12px;
height: 44px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius-pill);
display: flex;
align-items: center;
justify-content: space-around;
transition: left var(--motion-base) var(--ease), right var(--motion-base) var(--ease), bottom var(--motion-base) var(--ease), border-radius var(--motion-base) var(--ease);
}
.demo-bar.is-attached { left: 0; right: 0; bottom: 0; border-radius: 0; border-width: 1px 0 0; }
.demo-bar-item { width: 18px; height: 18px; border-radius: 6px; background: var(--surface3); transition: background var(--motion-fast) var(--ease); }
.demo-bar-item.is-on { background: var(--accent); }
.segmented {
display: inline-flex;
padding: 3px;
background: var(--surface);
border-radius: var(--radius-pill);
gap: 2px;
align-self: flex-start;
}
.segmented-btn {
border: none;
background: transparent;
padding: 8px 16px;
border-radius: var(--radius-pill);
font-family: var(--font-body);
font-size: 13px;
font-weight: 600;
color: var(--fg2);
cursor: pointer;
transition: background var(--motion-fast) var(--ease), color var(--motion-fast) var(--ease);
}
.segmented-btn.is-active { background: var(--accent-soft); color: var(--accent-text); }
.customize-list {
grid-column: 1 / -1;
background: var(--bg);
border-radius: var(--radius-card);
padding: 28px 32px;
}
.customize-list h3 { font-size: 18px; margin: 0 0 16px; }
.feature-list {
margin: 0;
padding: 0;
list-style: none;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 32px;
color: var(--fg2);
font-size: 15px;
}
.feature-list li { display: flex; align-items: center; gap: 10px; }
.feature-list li::before {
content: "";
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent);
flex: none;
}
@media (max-width: 760px) {
.customize-grid { grid-template-columns: 1fr; }
.feature-list { grid-template-columns: 1fr; }
}
/* ---------- privacy ---------- */
.privacy-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
gap: clamp(32px, 6vw, 64px);
align-items: center;
}
.privacy-points {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 14px;
}
.privacy-points li {
display: flex;
align-items: center;
gap: 14px;
background: var(--surface);
border-radius: var(--radius-input);
padding: 16px 18px;
font-weight: 500;
}
@media (max-width: 820px) {
.privacy-grid { grid-template-columns: 1fr; }
}
/* ---------- cta ---------- */
.section-cta { text-align: center; background: var(--surface); }
.cta-inner { display: flex; flex-direction: column; align-items: center; }
.cta-inner .section-sub { margin-bottom: 32px; }
.cta-actions { justify-content: center; }
.cta-fine { margin-top: 20px; font-size: 14px; color: var(--fg3); }
.cta-fine a { color: var(--accent-text); font-weight: 600; text-decoration: none; }
/* ---------- footer ---------- */
.site-footer { border-top: 1px solid var(--line); padding: 32px 0; }
.footer-row {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
}
.footer-note { color: var(--fg3); font-size: 13px; margin: 0; flex: 1; min-width: 200px; }
.footer-links { display: flex; gap: 20px; }
.footer-links a { font-size: 14px; text-decoration: none; color: var(--fg2); }
.footer-links a:hover { color: var(--fg1); }
/* ---------- motion ---------- */
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.phone, .chip, .btn { transition: none !important; }
}
.reveal {
opacity: 0;
transform: translateY(16px);
transition: opacity 600ms var(--ease), transform 600ms var(--ease);
}
.reveal.is-visible { opacity: 1; transform: none; }
+17
View File
@@ -0,0 +1,17 @@
# Cloudflare Workers config for the Noo site, served at noo.ayushya.dev.
# Deployed separately from the root worker (see .gitea/workflows/deploy.yml).
name = "noo"
compatibility_date = "2026-09-17"
# Custom domain: Cloudflare creates the DNS record and certificate for
# noo.ayushya.dev automatically, provided ayushya.dev is a zone on the same
# Cloudflare account as the API token used for deploys.
routes = [
{ pattern = "noo.ayushya.dev", custom_domain = true }
]
# Serves this directory as static assets; non-site files are listed in
# .assetsignore so they are not uploaded.
[assets]
directory = "."
not_found_handling = "404-page"
+5
View File
@@ -0,0 +1,5 @@
wrangler.toml
README.md
.assetsignore
node_modules
.wrangler
+9 -9
View File
@@ -1,9 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect x="2" y="2" width="60" height="60" rx="16" fill="#2563eb"/> <rect x="2" y="2" width="60" height="60" rx="16" fill="#2563eb"/>
<rect x="10" y="2" width="2" height="10" fill="#ffffff"/> <rect x="10" y="2" width="2" height="10" fill="#ffffff"/>
<circle cx="11" cy="2" r="3" fill="#ffffff"/> <circle cx="11" cy="2" r="3" fill="#ffffff"/>
<circle cx="22" cy="34" r="9" fill="#ffffff"/> <circle cx="22" cy="34" r="9" fill="#ffffff"/>
<circle cx="22" cy="34" r="3.4" fill="#2563eb"/> <circle cx="22" cy="34" r="3.4" fill="#2563eb"/>
<circle cx="42" cy="34" r="9" fill="#ffffff"/> <circle cx="42" cy="34" r="9" fill="#ffffff"/>
<circle cx="42" cy="34" r="3.4" fill="#2563eb"/> <circle cx="42" cy="34" r="3.4" fill="#2563eb"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 444 B

After

Width:  |  Height:  |  Size: 453 B

+399 -399
View File
@@ -1,399 +1,399 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ayushya.dev</title> <title>Ayushya.dev</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
:root{ :root{
--bg:#fafaf9; --fg:#141414; --muted:#6b6b68; --accent:#2563eb; --bg:#fafaf9; --fg:#141414; --muted:#6b6b68; --accent:#2563eb;
--border:#141414; --card-bg:#ffffff; --divider:#dcdcd8; --limb:#c7c7c2; --border:#141414; --card-bg:#ffffff; --divider:#dcdcd8; --limb:#c7c7c2;
} }
@media (prefers-color-scheme: dark){ @media (prefers-color-scheme: dark){
:root{ :root{
--bg:#0b0d0f; --fg:#f2f2f0; --muted:#9a9a95; --accent:#64a1fb; --bg:#0b0d0f; --fg:#f2f2f0; --muted:#9a9a95; --accent:#64a1fb;
--border:#f2f2f0; --card-bg:#141719; --divider:#2b2e31; --limb:#33373b; --border:#f2f2f0; --card-bg:#141719; --divider:#2b2e31; --limb:#33373b;
} }
} }
*{box-sizing:border-box;} *{box-sizing:border-box;}
html,body{margin:0;padding:0;} html,body{margin:0;padding:0;}
body{background:var(--bg);color:var(--fg);font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;} body{background:var(--bg);color:var(--fg);font-family:'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;}
a{color:inherit;text-decoration:none;} a{color:inherit;text-decoration:none;}
h1,h2,h3,p{margin:0;} h1,h2,h3,p{margin:0;}
button{font-family:inherit;} button{font-family:inherit;}
.page{width:100%;min-height:100vh;display:flex;flex-direction:column;} .page{width:100%;min-height:100vh;display:flex;flex-direction:column;}
.layout-grid{display:grid;grid-template-columns:1fr 190px 1fr;gap:48px;max-width:1180px;width:100%;margin:0 auto;padding:72px 32px 56px;align-items:start;} .layout-grid{display:grid;grid-template-columns:1fr 190px 1fr;gap:48px;max-width:1180px;width:100%;margin:0 auto;padding:72px 32px 56px;align-items:start;}
.name-box{border:1px solid var(--border);border-radius:14px;padding:26px 30px;width:100%;} .name-box{border:1px solid var(--border);border-radius:14px;padding:26px 30px;width:100%;}
.name-box h1{font-size:26px;font-weight:700;letter-spacing:0.03em;text-align:center;} .name-box h1{font-size:26px;font-weight:700;letter-spacing:0.03em;text-align:center;}
.left-col{display:flex;flex-direction:column;gap:32px;min-width:0;} .left-col{display:flex;flex-direction:column;gap:32px;min-width:0;}
.projects-block{display:flex;flex-direction:column;gap:16px;margin-top:48px;} .projects-block{display:flex;flex-direction:column;gap:16px;margin-top:48px;}
.stack-row{display:flex;align-items:center;gap:14px;position:relative;} .stack-row{display:flex;align-items:center;gap:14px;position:relative;}
.link-btn{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--accent);color:var(--accent);border-radius:8px;padding:11px 20px;font-size:13px;font-weight:600;letter-spacing:0.06em;font-family:inherit;background:transparent;cursor:pointer;transition:background .15s ease,color .15s ease;} .link-btn{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--accent);color:var(--accent);border-radius:8px;padding:11px 20px;font-size:13px;font-weight:600;letter-spacing:0.06em;font-family:inherit;background:transparent;cursor:pointer;transition:background .15s ease,color .15s ease;}
.link-btn:hover{background:var(--accent);color:var(--card-bg);} .link-btn:hover{background:var(--accent);color:var(--card-bg);}
.card-link{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--accent);color:var(--accent);border-radius:8px;padding:9px 18px;font-size:12px;font-weight:700;letter-spacing:0.06em;transition:background .15s ease,color .15s ease;align-self:flex-start;} .card-link{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--accent);color:var(--accent);border-radius:8px;padding:9px 18px;font-size:12px;font-weight:700;letter-spacing:0.06em;transition:background .15s ease,color .15s ease;align-self:flex-start;}
.card-link:hover{background:var(--accent);color:var(--card-bg);} .card-link:hover{background:var(--accent);color:var(--card-bg);}
.nav-btn{width:40px;height:40px;min-width:40px;min-height:40px;border-radius:50%;border:1px solid var(--border);background:var(--card-bg);color:var(--fg);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background .15s ease,color .15s ease;} .nav-btn{width:40px;height:40px;min-width:40px;min-height:40px;border-radius:50%;border:1px solid var(--border);background:var(--card-bg);color:var(--fg);display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;transition:background .15s ease,color .15s ease;}
.nav-btn:hover{background:var(--fg);color:var(--bg);} .nav-btn:hover{background:var(--fg);color:var(--bg);}
.nav-btn:hover .tri{border-bottom-color:var(--bg);} .nav-btn:hover .tri{border-bottom-color:var(--bg);}
.nav-btn:hover .tri-down{border-top-color:var(--bg);} .nav-btn:hover .tri-down{border-top-color:var(--bg);}
.nav-btn[data-disabled="true"]{opacity:0.32;pointer-events:none;} .nav-btn[data-disabled="true"]{opacity:0.32;pointer-events:none;}
.tri{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:8px solid var(--accent);transition:border-color .15s ease;} .tri{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:8px solid var(--accent);transition:border-color .15s ease;}
.tri-down{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:8px solid var(--accent);transition:border-color .15s ease;} .tri-down{width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:8px solid var(--accent);transition:border-color .15s ease;}
.nav-col{position:absolute;right:-58px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:10px;flex-shrink:0;} .nav-col{position:absolute;right:-58px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:10px;flex-shrink:0;}
.stack-scroll{scrollbar-width:none;-ms-overflow-style:none;} .stack-scroll{scrollbar-width:none;-ms-overflow-style:none;}
.stack-scroll::-webkit-scrollbar{display:none;width:0;height:0;} .stack-scroll::-webkit-scrollbar{display:none;width:0;height:0;}
.stack-card-top{display:flex;flex-direction:column;gap:14px;} .stack-card-top{display:flex;flex-direction:column;gap:14px;}
.avatar{width:52px;height:52px;border-radius:50%;border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--accent);flex-shrink:0;} .avatar{width:52px;height:52px;border-radius:50%;border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--accent);flex-shrink:0;}
.card-text{display:flex;flex-direction:column;gap:6px;} .card-text{display:flex;flex-direction:column;gap:6px;}
.card-text h3{font-size:15px;font-weight:700;letter-spacing:0.02em;} .card-text h3{font-size:15px;font-weight:700;letter-spacing:0.02em;}
.desc-clamp{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;font-size:13px;line-height:1.5;color:var(--muted);} .desc-clamp{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;font-size:13px;line-height:1.5;color:var(--muted);}
.projects-label{text-align:center;font-size:11px;letter-spacing:0.14em;color:var(--muted);} .projects-label{text-align:center;font-size:11px;letter-spacing:0.14em;color:var(--muted);}
.mid-col{display:flex;flex-direction:column;align-items:stretch;gap:16px;height:100%;align-self:stretch;} .mid-col{display:flex;flex-direction:column;align-items:stretch;gap:16px;height:100%;align-self:stretch;}
.divider-v{width:1px;flex:1 1 auto;background:var(--divider);margin:8px auto;} .divider-v{width:1px;flex:1 1 auto;background:var(--divider);margin:8px auto;}
.email-link{font-size:12px;letter-spacing:0.06em;color:var(--muted);text-align:center;} .email-link{font-size:12px;letter-spacing:0.06em;color:var(--muted);text-align:center;}
.robot-col{display:flex;flex-direction:column;align-items:center;gap:28px;padding-top:6px;} .robot-col{display:flex;flex-direction:column;align-items:center;gap:28px;padding-top:6px;}
.marquee-wrap{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);width:100%;max-width:260px;font-size:12px;letter-spacing:0.08em;color:var(--muted);} .marquee-wrap{overflow:hidden;-webkit-mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);mask-image:linear-gradient(90deg,transparent,#000 12%,#000 88%,transparent);width:100%;max-width:260px;font-size:12px;letter-spacing:0.08em;color:var(--muted);}
.marquee-track{display:inline-block;white-space:nowrap;animation:scrollLeft 16s linear infinite;} .marquee-track{display:inline-block;white-space:nowrap;animation:scrollLeft 16s linear infinite;}
@keyframes scrollLeft{from{transform:translateX(0);}to{transform:translateX(-50%);}} @keyframes scrollLeft{from{transform:translateX(0);}to{transform:translateX(-50%);}}
.robot{display:flex;flex-direction:column;align-items:center;} .robot{display:flex;flex-direction:column;align-items:center;}
.antenna-wire{width:2px;height:20px;background:var(--fg);} .antenna-wire{width:2px;height:20px;background:var(--fg);}
.antenna-seg-1{width:2px;height:12px;background:var(--fg);transform:rotate(28deg) translateX(3px);} .antenna-seg-1{width:2px;height:12px;background:var(--fg);transform:rotate(28deg) translateX(3px);}
.antenna-seg-2{width:2px;height:12px;background:var(--fg);transform:rotate(-28deg) translateX(-3px);margin-top:-2px;} .antenna-seg-2{width:2px;height:12px;background:var(--fg);transform:rotate(-28deg) translateX(-3px);margin-top:-2px;}
.antenna-tip{width:10px;height:10px;border-radius:50%;background:var(--fg);margin-top:2px;} .antenna-tip{width:10px;height:10px;border-radius:50%;background:var(--fg);margin-top:2px;}
.head{width:140px;height:118px;margin-top:14px;border:2px solid var(--fg);border-radius:30px;background:var(--card-bg);display:flex;align-items:center;justify-content:center;gap:20px;} .head{width:140px;height:118px;margin-top:14px;border:2px solid var(--fg);border-radius:30px;background:var(--card-bg);display:flex;align-items:center;justify-content:center;gap:20px;}
.eye-ball{width:34px;height:34px;border-radius:50%;background:var(--card-bg);border:2px solid var(--fg);position:relative;flex-shrink:0;} .eye-ball{width:34px;height:34px;border-radius:50%;background:var(--card-bg);border:2px solid var(--fg);position:relative;flex-shrink:0;}
.pupil{position:absolute;top:50%;left:50%;width:13px;height:13px;border-radius:50%;background:var(--fg);margin-top:-6.5px;margin-left:-6.5px;transition:transform .06s linear;} .pupil{position:absolute;top:50%;left:50%;width:13px;height:13px;border-radius:50%;background:var(--fg);margin-top:-6.5px;margin-left:-6.5px;transition:transform .06s linear;}
.neck-gap{height:16px;} .neck-gap{height:16px;}
.body-row{display:flex;align-items:flex-start;gap:10px;} .body-row{display:flex;align-items:flex-start;gap:10px;}
.arm{width:16px;height:118px;border-radius:10px;background:var(--limb);margin-top:10px;} .arm{width:16px;height:118px;border-radius:10px;background:var(--limb);margin-top:10px;}
.torso{width:150px;height:150px;border:2px solid var(--fg);border-radius:26px;background:var(--card-bg);} .torso{width:150px;height:150px;border:2px solid var(--fg);border-radius:26px;background:var(--card-bg);}
.leg-gap{height:10px;} .leg-gap{height:10px;}
.legs-row{display:flex;gap:26px;} .legs-row{display:flex;gap:26px;}
.leg{width:20px;height:78px;border-radius:10px;background:var(--limb);} .leg{width:20px;height:78px;border-radius:10px;background:var(--limb);}
@media (max-width: 880px){ @media (max-width: 880px){
.layout-grid{grid-template-columns:1fr !important;} .layout-grid{grid-template-columns:1fr !important;}
.divider-v{display:none !important;} .divider-v{display:none !important;}
.robot-col{padding-top:0 !important;} .robot-col{padding-top:0 !important;}
.stack-row{flex-direction:column;align-items:stretch;} .stack-row{flex-direction:column;align-items:stretch;}
.nav-col{flex-direction:row !important;justify-content:center;position:static;transform:none;right:auto;top:auto;} .nav-col{flex-direction:row !important;justify-content:center;position:static;transform:none;right:auto;top:auto;}
.tri{transform:rotate(-90deg);} .tri{transform:rotate(-90deg);}
.tri-down{transform:rotate(-90deg);} .tri-down{transform:rotate(-90deg);}
} }
</style> </style>
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<div class="layout-grid"> <div class="layout-grid">
<div class="left-col"> <div class="left-col">
<div class="name-box"><h1>AYUSHYA.DEV</h1></div> <div class="name-box"><h1>AYUSHYA.DEV</h1></div>
<div class="projects-block"> <div class="projects-block">
<div class="stack-row"> <div class="stack-row">
<div class="stack-scroll" id="stackScroll"> <div class="stack-scroll" id="stackScroll">
<div class="spacer" id="spacer" style="position:relative;"> <div class="spacer" id="spacer" style="position:relative;">
<div class="layer" id="layer" style="position:absolute;top:0;left:0;"> <div class="layer" id="layer" style="position:absolute;top:0;left:0;">
<!-- cards injected by JS --> <!-- cards injected by JS -->
</div> </div>
</div> </div>
</div> </div>
<div class="nav-col"> <div class="nav-col">
<button class="nav-btn" id="prevBtn" aria-label="Show previous project" type="button"> <button class="nav-btn" id="prevBtn" aria-label="Show previous project" type="button">
<span class="tri"></span> <span class="tri"></span>
</button> </button>
<button class="nav-btn" id="nextBtn" aria-label="Show next project" type="button"> <button class="nav-btn" id="nextBtn" aria-label="Show next project" type="button">
<span class="tri-down"></span> <span class="tri-down"></span>
</button> </button>
</div> </div>
</div> </div>
<p class="projects-label">PROJECTS</p> <p class="projects-label">PROJECTS</p>
</div> </div>
</div> </div>
<div class="mid-col"> <div class="mid-col">
<a class="link-btn" href="https://github.com/ayushyamitabh/" target="_blank" rel="noopener noreferrer">GITHUB</a> <a class="link-btn" href="https://github.com/ayushyamitabh/" target="_blank" rel="noopener noreferrer">GITHUB</a>
<a class="link-btn" href="https://linkedin.com/in/ayushyamitabh" target="_blank" rel="noopener noreferrer">LINKEDIN</a> <a class="link-btn" href="https://linkedin.com/in/ayushyamitabh" target="_blank" rel="noopener noreferrer">LINKEDIN</a>
<div class="divider-v"></div> <div class="divider-v"></div>
<a class="email-link" href="mailto:hello@ayushya.dev">HELLO@AYUSHYA.DEV</a> <a class="email-link" href="mailto:hello@ayushya.dev">HELLO@AYUSHYA.DEV</a>
</div> </div>
<div class="robot-col"> <div class="robot-col">
<div class="marquee-wrap"> <div class="marquee-wrap">
<span class="marquee-track" id="marquee"></span> <span class="marquee-track" id="marquee"></span>
</div> </div>
<div class="robot"> <div class="robot">
<div class="antenna-wire"></div> <div class="antenna-wire"></div>
<div class="antenna-seg-1"></div> <div class="antenna-seg-1"></div>
<div class="antenna-seg-2"></div> <div class="antenna-seg-2"></div>
<div class="antenna-tip"></div> <div class="antenna-tip"></div>
<div class="head"> <div class="head">
<div class="eye-ball" id="eyeLeft"><div class="pupil" id="pupilLeft"></div></div> <div class="eye-ball" id="eyeLeft"><div class="pupil" id="pupilLeft"></div></div>
<div class="eye-ball" id="eyeRight"><div class="pupil" id="pupilRight"></div></div> <div class="eye-ball" id="eyeRight"><div class="pupil" id="pupilRight"></div></div>
</div> </div>
<div class="neck-gap"></div> <div class="neck-gap"></div>
<div class="body-row"> <div class="body-row">
<div class="arm"></div> <div class="arm"></div>
<div class="torso"></div> <div class="torso"></div>
<div class="arm"></div> <div class="arm"></div>
</div> </div>
<div class="leg-gap"></div> <div class="leg-gap"></div>
<div class="legs-row"> <div class="legs-row">
<div class="leg"></div> <div class="leg"></div>
<div class="leg"></div> <div class="leg"></div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
(function () { (function () {
var PROJECTS = [ var PROJECTS = [
{ initials: 'N', name: 'NOO', desc: 'A fast, native Nextcloud client built with Flutter and Material You \u2014 files, photos, activity, shares, and trash in one themed app.', link: 'https://noo.ayushya.dev' }, { initials: 'N', name: 'NOO', desc: 'A fast, native Nextcloud client built with Flutter and Material You \u2014 files, photos, activity, shares, and trash in one themed app.', link: 'https://noo.ayushya.dev' },
{ initials: 'WP', name: 'CHROME WATCH PARTY', desc: 'Syncs streamed video across devices so you can watch with your favorite people, with chat, GIFs, and free group calls.', link: 'https://watchparty-archive.web.app/' }, { initials: 'WP', name: 'CHROME WATCH PARTY', desc: 'Syncs streamed video across devices so you can watch with your favorite people, with chat, GIFs, and free group calls.', link: 'https://watchparty-archive.web.app/' },
{ initials: 'V&M', name: 'VAX & ME', desc: 'Explains the expected side effects of COVID-19 vaccines, built from FDA data on Pfizer, Moderna, and J&J.', link: 'https://vaxandme-archive.web.app/' }, { initials: 'V&M', name: 'VAX & ME', desc: 'Explains the expected side effects of COVID-19 vaccines, built from FDA data on Pfizer, Moderna, and J&J.', link: 'https://vaxandme-archive.web.app/' },
]; ];
var COUNT = PROJECTS.length; var COUNT = PROJECTS.length;
var PEEK_SCALE = 0.9; var PEEK_SCALE = 0.9;
var MOBILE_BREAKPOINT = 880; var MOBILE_BREAKPOINT = 880;
var MOBILE_CONTAINER_HEIGHT = 260; var MOBILE_CONTAINER_HEIGHT = 260;
var DESKTOP_STEP = 160; var DESKTOP_STEP = 160;
var DESKTOP_VIEW = 318; var DESKTOP_VIEW = 318;
var DESKTOP_CARD = 250; var DESKTOP_CARD = 250;
var stackScroll = document.getElementById('stackScroll'); var stackScroll = document.getElementById('stackScroll');
var spacer = document.getElementById('spacer'); var spacer = document.getElementById('spacer');
var layer = document.getElementById('layer'); var layer = document.getElementById('layer');
var prevBtn = document.getElementById('prevBtn'); var prevBtn = document.getElementById('prevBtn');
var nextBtn = document.getElementById('nextBtn'); var nextBtn = document.getElementById('nextBtn');
var cardEls = []; var cardEls = [];
var isMobile = window.matchMedia('(max-width: ' + MOBILE_BREAKPOINT + 'px)').matches; var isMobile = window.matchMedia('(max-width: ' + MOBILE_BREAKPOINT + 'px)').matches;
var containerWidth = 320; var containerWidth = 320;
var scrollPos = 0; var scrollPos = 0;
function escapeHtml(str) { function escapeHtml(str) {
var div = document.createElement('div'); var div = document.createElement('div');
div.textContent = str; div.textContent = str;
return div.innerHTML; return div.innerHTML;
} }
function buildCards() { function buildCards() {
PROJECTS.forEach(function (proj) { PROJECTS.forEach(function (proj) {
var card = document.createElement('div'); var card = document.createElement('div');
card.innerHTML = card.innerHTML =
'<div class="stack-card-top">' + '<div class="stack-card-top">' +
'<div class="avatar">' + escapeHtml(proj.initials) + '</div>' + '<div class="avatar">' + escapeHtml(proj.initials) + '</div>' +
'<div class="card-text">' + '<div class="card-text">' +
'<h3>' + escapeHtml(proj.name) + '</h3>' + '<h3>' + escapeHtml(proj.name) + '</h3>' +
'<p class="desc-clamp">' + escapeHtml(proj.desc) + '</p>' + '<p class="desc-clamp">' + escapeHtml(proj.desc) + '</p>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<a class="card-link" href="' + proj.link + '" target="_blank" rel="noopener noreferrer">LINK</a>'; '<a class="card-link" href="' + proj.link + '" target="_blank" rel="noopener noreferrer">LINK</a>';
layer.appendChild(card); layer.appendChild(card);
cardEls.push(card); cardEls.push(card);
}); });
} }
function getGeometry() { function getGeometry() {
if (isMobile) { if (isMobile) {
var cw = containerWidth; var cw = containerWidth;
var peek = Math.max(16, Math.round(cw * 0.1)); var peek = Math.max(16, Math.round(cw * 0.1));
var cardSize = Math.round(Math.max(cw - peek * 2, 120)); var cardSize = Math.round(Math.max(cw - peek * 2, 120));
var step = Math.round(cardSize * 0.64); var step = Math.round(cardSize * 0.64);
return { axisProp: 'left', sizeProp: 'width', crossSizeProp: 'height', peek: peek, cardSize: cardSize, step: step, viewSize: cw }; return { axisProp: 'left', sizeProp: 'width', crossSizeProp: 'height', peek: peek, cardSize: cardSize, step: step, viewSize: cw };
} }
return { axisProp: 'top', sizeProp: 'height', crossSizeProp: 'width', peek: 34, cardSize: DESKTOP_CARD, step: DESKTOP_STEP, viewSize: DESKTOP_VIEW }; return { axisProp: 'top', sizeProp: 'height', crossSizeProp: 'width', peek: 34, cardSize: DESKTOP_CARD, step: DESKTOP_STEP, viewSize: DESKTOP_VIEW };
} }
function lerp(a, b, t) { return a + (b - a) * t; } function lerp(a, b, t) { return a + (b - a) * t; }
var EPS = 1.05; var EPS = 1.05;
function render() { function render() {
var g = getGeometry(); var g = getGeometry();
var maxScroll = (COUNT - 1) * g.step; var maxScroll = (COUNT - 1) * g.step;
var clampedPos = Math.max(0, Math.min(scrollPos, maxScroll)); var clampedPos = Math.max(0, Math.min(scrollPos, maxScroll));
var progress = clampedPos / g.step; var progress = clampedPos / g.step;
if (isMobile) { if (isMobile) {
stackScroll.style.cssText = 'position:relative;height:' + MOBILE_CONTAINER_HEIGHT + 'px;width:100%;overflow-x:auto;overflow-y:hidden;min-width:0;-webkit-overflow-scrolling:touch;'; stackScroll.style.cssText = 'position:relative;height:' + MOBILE_CONTAINER_HEIGHT + 'px;width:100%;overflow-x:auto;overflow-y:hidden;min-width:0;-webkit-overflow-scrolling:touch;';
spacer.style.cssText = 'position:relative;width:' + (g.viewSize + maxScroll) + 'px;height:100%;'; spacer.style.cssText = 'position:relative;width:' + (g.viewSize + maxScroll) + 'px;height:100%;';
layer.style.cssText = 'position:absolute;top:0;left:0;width:' + g.viewSize + 'px;height:100%;transform:translateX(' + clampedPos + 'px);'; layer.style.cssText = 'position:absolute;top:0;left:0;width:' + g.viewSize + 'px;height:100%;transform:translateX(' + clampedPos + 'px);';
} else { } else {
stackScroll.style.cssText = 'position:relative;height:' + g.viewSize + 'px;flex:1 1 auto;min-width:0;overflow-y:auto;overflow-x:hidden;'; stackScroll.style.cssText = 'position:relative;height:' + g.viewSize + 'px;flex:1 1 auto;min-width:0;overflow-y:auto;overflow-x:hidden;';
spacer.style.cssText = 'position:relative;height:' + (g.viewSize + maxScroll) + 'px;width:100%;'; spacer.style.cssText = 'position:relative;height:' + (g.viewSize + maxScroll) + 'px;width:100%;';
layer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:' + g.viewSize + 'px;transform:translateY(' + clampedPos + 'px);'; layer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:' + g.viewSize + 'px;transform:translateY(' + clampedPos + 'px);';
} }
for (var i = 0; i < COUNT; i++) { for (var i = 0; i < COUNT; i++) {
var delta = i - progress; var delta = i - progress;
var pos, scale, opacity; var pos, scale, opacity;
if (delta < -EPS || delta > EPS) { if (delta < -EPS || delta > EPS) {
pos = g.peek; scale = PEEK_SCALE; opacity = 0; pos = g.peek; scale = PEEK_SCALE; opacity = 0;
} else if (delta <= 0) { } else if (delta <= 0) {
var t1 = Math.min(-delta, 1); var t1 = Math.min(-delta, 1);
pos = lerp(g.peek, 0, t1); pos = lerp(g.peek, 0, t1);
scale = lerp(1, PEEK_SCALE, t1); scale = lerp(1, PEEK_SCALE, t1);
opacity = lerp(1, 0.55, t1); opacity = lerp(1, 0.55, t1);
} else { } else {
var t2 = Math.min(delta, 1); var t2 = Math.min(delta, 1);
pos = lerp(g.peek, g.peek * 2, t2); pos = lerp(g.peek, g.peek * 2, t2);
scale = lerp(1, PEEK_SCALE, t2); scale = lerp(1, PEEK_SCALE, t2);
opacity = lerp(1, 0.55, t2); opacity = lerp(1, 0.55, t2);
} }
var zIndex = Math.round((1 - Math.min(Math.abs(delta), 1)) * 100); var zIndex = Math.round((1 - Math.min(Math.abs(delta), 1)) * 100);
var pointerEvents = Math.abs(delta) < 0.05 ? 'auto' : 'none'; var pointerEvents = Math.abs(delta) < 0.05 ? 'auto' : 'none';
var otherAxis = isMobile ? 'top:0;' : 'left:0;'; var otherAxis = isMobile ? 'top:0;' : 'left:0;';
var sizeStyle = g.sizeProp + ':' + g.cardSize + 'px; ' + g.crossSizeProp + ':100%;'; var sizeStyle = g.sizeProp + ':' + g.cardSize + 'px; ' + g.crossSizeProp + ':100%;';
cardEls[i].style.cssText = cardEls[i].style.cssText =
'position:absolute; ' + g.axisProp + ':' + pos + 'px; ' + otherAxis + ' ' + sizeStyle + 'position:absolute; ' + g.axisProp + ':' + pos + 'px; ' + otherAxis + ' ' + sizeStyle +
' z-index:' + zIndex + '; opacity:' + opacity + '; pointer-events:' + pointerEvents + '; transform: scale(' + scale + ');' + ' z-index:' + zIndex + '; opacity:' + opacity + '; pointer-events:' + pointerEvents + '; transform: scale(' + scale + ');' +
' padding:24px; background:var(--card-bg); border:1px solid var(--border); border-radius:16px; display:flex; flex-direction:column; justify-content:space-between; gap:14px; box-shadow:0 10px 24px rgba(0,0,0,0.12);'; ' padding:24px; background:var(--card-bg); border:1px solid var(--border); border-radius:16px; display:flex; flex-direction:column; justify-content:space-between; gap:14px; box-shadow:0 10px 24px rgba(0,0,0,0.12);';
} }
var activeIndex = Math.round(progress); var activeIndex = Math.round(progress);
prevBtn.setAttribute('data-disabled', activeIndex <= 0 ? 'true' : 'false'); prevBtn.setAttribute('data-disabled', activeIndex <= 0 ? 'true' : 'false');
nextBtn.setAttribute('data-disabled', activeIndex >= COUNT - 1 ? 'true' : 'false'); nextBtn.setAttribute('data-disabled', activeIndex >= COUNT - 1 ? 'true' : 'false');
} }
function getStep() { function getStep() {
return getGeometry().step; return getGeometry().step;
} }
function nextCard() { function nextCard() {
var step = getStep(); var step = getStep();
var active = Math.round(scrollPos / step); var active = Math.round(scrollPos / step);
var target = Math.min(active + 1, COUNT - 1) * step; var target = Math.min(active + 1, COUNT - 1) * step;
if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' }); if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' });
else stackScroll.scrollTo({ top: target, behavior: 'smooth' }); else stackScroll.scrollTo({ top: target, behavior: 'smooth' });
} }
function prevCard() { function prevCard() {
var step = getStep(); var step = getStep();
var active = Math.round(scrollPos / step); var active = Math.round(scrollPos / step);
var target = Math.max(active - 1, 0) * step; var target = Math.max(active - 1, 0) * step;
if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' }); if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' });
else stackScroll.scrollTo({ top: target, behavior: 'smooth' }); else stackScroll.scrollTo({ top: target, behavior: 'smooth' });
} }
function measure() { function measure() {
var rect = stackScroll.getBoundingClientRect(); var rect = stackScroll.getBoundingClientRect();
if (rect.width > 0) containerWidth = rect.width; if (rect.width > 0) containerWidth = rect.width;
render(); render();
} }
buildCards(); buildCards();
measure(); measure();
prevBtn.addEventListener('click', prevCard); prevBtn.addEventListener('click', prevCard);
nextBtn.addEventListener('click', nextCard); nextBtn.addEventListener('click', nextCard);
var scrollRafPending = false; var scrollRafPending = false;
var snapTimer = null; var snapTimer = null;
function snapToNearest() { function snapToNearest() {
var step = getStep(); var step = getStep();
var maxIndex = COUNT - 1; var maxIndex = COUNT - 1;
var nearest = Math.max(0, Math.min(Math.round(scrollPos / step), maxIndex)); var nearest = Math.max(0, Math.min(Math.round(scrollPos / step), maxIndex));
var target = nearest * step; var target = nearest * step;
if (Math.abs(scrollPos - target) > 1) { if (Math.abs(scrollPos - target) > 1) {
if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' }); if (isMobile) stackScroll.scrollTo({ left: target, behavior: 'smooth' });
else stackScroll.scrollTo({ top: target, behavior: 'smooth' }); else stackScroll.scrollTo({ top: target, behavior: 'smooth' });
} }
} }
stackScroll.addEventListener('scroll', function () { stackScroll.addEventListener('scroll', function () {
if (scrollRafPending) return; if (scrollRafPending) return;
scrollRafPending = true; scrollRafPending = true;
requestAnimationFrame(function () { requestAnimationFrame(function () {
scrollRafPending = false; scrollRafPending = false;
scrollPos = isMobile ? stackScroll.scrollLeft : stackScroll.scrollTop; scrollPos = isMobile ? stackScroll.scrollLeft : stackScroll.scrollTop;
render(); render();
}); });
clearTimeout(snapTimer); clearTimeout(snapTimer);
snapTimer = setTimeout(snapToNearest, 130); snapTimer = setTimeout(snapToNearest, 130);
}); });
var mq = window.matchMedia('(max-width: ' + MOBILE_BREAKPOINT + 'px)'); var mq = window.matchMedia('(max-width: ' + MOBILE_BREAKPOINT + 'px)');
function handleMqChange(e) { function handleMqChange(e) {
stackScroll.scrollTo({ top: 0, left: 0 }); stackScroll.scrollTo({ top: 0, left: 0 });
isMobile = e.matches; isMobile = e.matches;
scrollPos = 0; scrollPos = 0;
measure(); measure();
} }
if (mq.addEventListener) mq.addEventListener('change', handleMqChange); if (mq.addEventListener) mq.addEventListener('change', handleMqChange);
else if (mq.addListener) mq.addListener(handleMqChange); else if (mq.addListener) mq.addListener(handleMqChange);
window.addEventListener('resize', measure); window.addEventListener('resize', measure);
if (typeof ResizeObserver !== 'undefined') { if (typeof ResizeObserver !== 'undefined') {
new ResizeObserver(measure).observe(stackScroll); new ResizeObserver(measure).observe(stackScroll);
} }
var binary = (function (len) { var binary = (function (len) {
var out = ''; var out = '';
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
out += Math.random() < 0.5 ? '0' : '1'; out += Math.random() < 0.5 ? '0' : '1';
if (i % 4 === 3) out += ' '; if (i % 4 === 3) out += ' ';
} }
return out; return out;
})(80); })(80);
document.getElementById('marquee').textContent = binary + ' ' + binary; document.getElementById('marquee').textContent = binary + ' ' + binary;
var eyeLeft = document.getElementById('eyeLeft'); var eyeLeft = document.getElementById('eyeLeft');
var eyeRight = document.getElementById('eyeRight'); var eyeRight = document.getElementById('eyeRight');
var pupilLeft = document.getElementById('pupilLeft'); var pupilLeft = document.getElementById('pupilLeft');
var pupilRight = document.getElementById('pupilRight'); var pupilRight = document.getElementById('pupilRight');
var MAX_OFFSET = 8; var MAX_OFFSET = 8;
var rafPending = false; var rafPending = false;
function updatePupils(clientX, clientY) { function updatePupils(clientX, clientY) {
[[eyeLeft, pupilLeft], [eyeRight, pupilRight]].forEach(function (pair) { [[eyeLeft, pupilLeft], [eyeRight, pupilRight]].forEach(function (pair) {
var eye = pair[0], pupil = pair[1]; var eye = pair[0], pupil = pair[1];
var rect = eye.getBoundingClientRect(); var rect = eye.getBoundingClientRect();
var cx = rect.left + rect.width / 2; var cx = rect.left + rect.width / 2;
var cy = rect.top + rect.height / 2; var cy = rect.top + rect.height / 2;
var angle = Math.atan2(clientY - cy, clientX - cx); var angle = Math.atan2(clientY - cy, clientX - cx);
pupil.style.transform = 'translate(' + (Math.cos(angle) * MAX_OFFSET) + 'px, ' + (Math.sin(angle) * MAX_OFFSET) + 'px)'; pupil.style.transform = 'translate(' + (Math.cos(angle) * MAX_OFFSET) + 'px, ' + (Math.sin(angle) * MAX_OFFSET) + 'px)';
}); });
} }
window.addEventListener('mousemove', function (e) { window.addEventListener('mousemove', function (e) {
if (rafPending) return; if (rafPending) return;
rafPending = true; rafPending = true;
var clientX = e.clientX, clientY = e.clientY; var clientX = e.clientX, clientY = e.clientY;
requestAnimationFrame(function () { requestAnimationFrame(function () {
rafPending = false; rafPending = false;
updatePupils(clientX, clientY); updatePupils(clientX, clientY);
}); });
}); });
})(); })();
</script> </script>
</body> </body>
</html> </html>
+11
View File
@@ -0,0 +1,11 @@
# Cloudflare Workers config for the portfolio site, served at ayushya.dev.
# Deployed separately from the noo worker (see .gitea/workflows/deploy-portfolio.yml).
name = "portfolio"
compatibility_date = "2026-09-17"
# Serves this directory as static assets; non-site files (including
# node_modules created by `npm install` during CI) are listed in
# .assetsignore so they are not uploaded.
[assets]
directory = "."
not_found_handling = "404-page"
-15
View File
@@ -1,15 +0,0 @@
# Cloudflare Workers config for the static portfolio site.
# Replace `name` with your existing Worker's name in the Cloudflare dashboard
# if one already exists at portfolio.ayushyamitabh.workers.dev — the name here
# determines which Worker gets deployed to.
name = "portfolio"
# Set this to the date you first deploy; Wrangler uses it to pin runtime behavior.
compatibility_date = "2026-09-17"
# Serves every file in this directory as a static asset. Kept in ./public
# (rather than the repo root) so that node_modules created by `npm install`
# during CI deploys is never picked up as a site asset.
[assets]
directory = "./public"
not_found_handling = "404-page"