From 19c82c50688bac33763c1bd67b89ee9947b42055 Mon Sep 17 00:00:00 2001 From: ayushya Date: Thu, 1 Oct 2026 10:49:35 -0400 Subject: [PATCH] Rebuild Noo site with interactive mockups and add privacy policy Use the app's monochrome icon, add live phone mockups for accounts, locks, sync, sharing and appearance options, a tab list, coming-soon platforms, and a full privacy policy page. Co-Authored-By: Claude Code --- noo/README.md | 27 +- noo/favicon.svg | 5 +- noo/icons.svg | 81 ++++ noo/index.html | 679 +++++++++++++++------------- noo/noo-mark.png | Bin 0 -> 37625 bytes noo/privacy.html | 175 ++++++++ noo/script.js | 931 +++++++++++++++++++++++++++++++++------ noo/styles.css | 1096 +++++++++++++++++++++++++++++----------------- 8 files changed, 2118 insertions(+), 876 deletions(-) create mode 100644 noo/icons.svg create mode 100644 noo/noo-mark.png create mode 100644 noo/privacy.html diff --git a/noo/README.md b/noo/README.md index 2ef88ab..cc7ec2a 100644 --- a/noo/README.md +++ b/noo/README.md @@ -10,21 +10,23 @@ product. ## Files -- `index.html` — the whole page (hero, features, customize, privacy, CTA). -- `styles.css` — all styling, incl. the design tokens as CSS custom +- `index.html` — the landing page (hero, feature showcases, the + Customize lab, tab list, privacy teaser, download CTA). +- `privacy.html` — the full privacy policy. +- `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. + explicit `data-theme` override). +- `script.js` — renders the interactive phone mockups from the design + tokens (bottom bar, nav menu, tabs, accents, AMOLED, action bar, + swiping, share sheet, locks, sync, accounts), plus nav and reveal. +- `icons.svg` — SVG sprite of the Lucide icons the mockups use + (``; needs an http server, not `file://`). +- `noo-mark.png` — the app's monochrome icon, tinted via CSS mask. - `favicon.svg`. ## Local preview -Any static file server works, e.g.: - ```bash -cd website python3 -m http.server 8000 ``` @@ -32,7 +34,6 @@ 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. +Deployed as Cloudflare Workers static assets (`wrangler.toml`); nothing +to build. Replace the `#` download links in `index.html` once real +release links exist. diff --git a/noo/favicon.svg b/noo/favicon.svg index 5c4fdfb..d9e63ec 100644 --- a/noo/favicon.svg +++ b/noo/favicon.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/noo/icons.svg b/noo/icons.svg new file mode 100644 index 0000000..30c83bf --- /dev/null +++ b/noo/icons.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/noo/index.html b/noo/index.html index ae0dc0e..1c1d6d7 100644 --- a/noo/index.html +++ b/noo/index.html @@ -1,307 +1,372 @@ - - - - - -Noo — a Nextcloud client that feels like it belongs on your phone - - - - - - - - - - - - - - - - - - -
- - -
-
-
-

For Nextcloud, unofficial

-

Your cloud.
Looking like yours.

-

- 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. -

- -
    -
  • No password stored — Login Flow v2
  • -
  • Offline device sync
  • -
  • Open source
  • -
-
- - -
-
- - -
-
-

Features

-

Everything your Nextcloud does. Nothing it doesn't.

-

- Files, photos, sharing and sync — built as a real mobile app, not a - browser tab pinned to your home screen. -

- -
-
- -

Files, browsed properly

-

Grid or list, sorted your way, with the same filters and - controls whether you're picking a file, uploading, or moving - one into place.

-
-
- -

Photos, grouped by month

-

Every image and video across your account in one gallery — - filter to just photos, just videos, or just favorites.

-
-
- -

Offline, for real

-

Pick folders to mirror locally and keep browsing — reading, - opening, previewing — with no connection at all.

-
-
- -

Sharing, without the detour

-

Share with people or a public link, see who has access, and - copy that link — all from one sheet, not three menus.

-
-
- -

Activity & recent

-

What changed, and where — grouped into today, yesterday and - this week, so catching up takes seconds.

-
-
- -

Multiple accounts

-

Switch between personal and work Nextcloud instances with a - swipe — each with its own sync, cache and lock.

-
-
-
-
- - -
-
-

Customize

-

Shape it around your thumb, not the other way round.

-

- Every screen bends to a handful of real settings — try a few - right here. -

- -
- -
-
-
- - -
-
Active tab
-
-
-
-
-

Pick your accent

-

Six built-in colors, or match your wallpaper with dynamic - color on Android.

-
- - - - - - -
-
-
- -
-
-
-
- - - - -
-
-
-

Floating or attached

-

The bottom bar can sit flush with the edge, or float free - with rounded corners — your call.

-
- - -
-
-
- -
-

And the rest

-
    -
  • Reorder tabs, hide the ones you never open
  • -
  • Move Search into the bottom bar
  • -
  • Light, dark, or match the system
  • -
  • Pure black backgrounds in dark mode
  • -
  • Choose what swiping a file left or right does
  • -
  • Reorder the bulk-action bar for multi-select
  • -
-
- -
-
-
- - -
-
-
-

Privacy

-

It's your server. Noo just visits.

-

- 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. -

-
-
    -
  • No password stored on device
  • -
  • Talks only to your own server
  • -
  • Open source, auditable
  • -
  • No analytics, no trackers
  • -
-
-
- - -
-
-

Bring your Nextcloud with you.

-

Free and open source. Point it at your server and go.

- -

Or build it from source — it's just Flutter.

-
-
- -
- -
- -
- - - - + + + + + +Noo — a Nextcloud client that feels like it belongs on your phone + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+

For Nextcloud, unofficial

+

Your cloud.
Looking like yours.

+

+ 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. +

+ +
    +
  • No password stored — Login Flow v2
  • +
  • Talks only to your server
  • +
  • No analytics, no trackers
  • +
+
+ +
+
+
+ + + + + +
+
+
+ + +
+
+

Features

+

Everything your Nextcloud does. Nothing it doesn't.

+

+ Built as a real mobile app, not a browser tab pinned to your home + screen. Every demo below is live — poke at it. +

+ + +
+
+ +

Multiple accounts, one tap apart

+

Keep your personal server, your work instance and the family + box signed in at once. Switch from the avatar or the Settings + list — each account keeps its own sync scope, cache and + favorites, and you can lock switching behind your fingerprint.

+

Tap an account to make it active.

+
+
+
+
Accounts
+
+
+
+
+ + +
+
+ +

Locks where you want them

+

Gate opening the app, switching accounts, and revealing hidden + files behind your device's own PIN, pattern or biometric. Noo + never stores or even sees a PIN — it just asks the OS whether you + are you.

+

The two extra locks need the main one on, just like in the app.

+
+
+
+
Security
+
+
+
+
+ + +
+
+ +

Offline and background sync

+

Mirror a few folders — or the whole account — to the device and + keep reading, opening and previewing with no connection. Background + sync tops them up while you aren't looking, on Wi-Fi only unless + you say otherwise, and it tells you when something conflicts.

+

Flip the options, then hit Sync now.

+
+
+
+
File sync
+
+
+
+
+ + +
+
+ +

Sharing that feels like Nextcloud

+

The same share sheet you know from the web and the official + app: invite people or groups with a permission pill, switch a public + link on and copy it, or skip all that and send the file's bytes + straight to another app. One sheet, not three menus.

+

Change a permission, remove someone, toggle the link.

+
+
+
+
+
+
+
+ + +
+
+

Customize

+

Shape it around your thumb, not the other way round.

+

+ This is the real Noo layout, drawn from the app's own design + tokens. Change a setting on the right and watch the phone. +

+ +
+
+
+

Tap the tabs, the menu and the avatar — they work.

+
+ +
+ +
+

Themes

+

Follow the system, or pick light or dark.

+
+ + + +
+
+ +
+

Colors

+

Six built-in accents, or let Android's wallpaper colors take over.

+
+
+ AMOLED blackPure black backgrounds in dark mode + +
+
+ +
+

Bottom bar

+

Flush with the edge, or floating free. Optionally park Search in the bar too.

+
+ + +
+
+ Search in bottom barTakes one tab slot and clears the top bar + +
+
+ +
+

Navigation menu

+

A hamburger that opens a drawer, or fold everything into the avatar.

+
+ + +
+
+ +
+

Tabs

+

+
    +
    +
    +
    + +
    +
    +

    Action bar

    +

    Decide which bulk actions live in the multi-select bar. The first three sit inline; the rest wait behind “More”. Drag to reorder and watch the bar.

    +
    +
      +
      + +
      +

      Swiping

      +

      Pick what swiping a file does. Drag a row — release past halfway to peek, or all the way through to trigger it.

      +
      + Swipe right +
      + + + + +
      +
      +
      + Swipe left +
      + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + +
      +
      +

      Tabs

      +

      Eight tabs. Pin the five you use.

      +

      + Every view below is a tab you can pin to the bottom bar, reorder, + or leave tucked in the menu. +

      +
      +

      Files

      Browse, upload, sort and filter every folder.

      +

      Photos

      Every image and video, grouped by month.

      +

      Favorites

      Everything you've starred, in one list.

      +

      Activity

      What changed and who changed it.

      +

      Trash

      Restore or permanently delete removed files.

      +

      Shares

      What you've shared, and what's shared with you.

      +

      Recent

      Recently edited and opened, grouped by day.

      +

      Offline

      Synced folders and files, no connection required.

      +
      +
      +
      + + +
      +
      +
      +

      Privacy

      +

      It's your server. Noo just visits.

      +

      + 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. The app talks to + your server and nowhere else, and collects nothing about you. +

      +

      Read the full privacy policy →

      +
      +
        +
      • No password stored on device
      • +
      • Talks only to your own server
      • +
      • No analytics, no trackers, no ads
      • +
      • Your data never touches our servers — we don't have any
      • +
      +
      +
      + + +
      +
      +

      Bring your Nextcloud with you.

      +

      Free. Point it at your server and go.

      + +
        +
      • macOS coming soon
      • +
      • Windows coming soon
      • +
      • iOS coming soon
      • +
      +

      + + Noo is open source. Read the code, file an issue, or build it yourself on + + GitHub. +

      +
      +
      + +
      + + + + + + diff --git a/noo/noo-mark.png b/noo/noo-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..bf95404c701334719f35cbb00a1f79185a450365 GIT binary patch literal 37625 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 + + + + +Privacy Policy — Noo + + + + + + + + + + + + + + +
      +
      +

      Legal

      +

      Privacy Policy

      +

      Effective date: 1 October 2026 · Applies to the Noo mobile application and to this website

      + +

      In short: Noo talks to one place only — the Nextcloud server you tell it to use. We do not operate any servers that receive your data, we do not use analytics, advertising, crash-reporting or tracking services, and we do not collect, store, sell, share or have access to your personal information.

      + + + +

      1. Definitions

      +

      In this policy, “Noo”, “the App” means the Noo Nextcloud client application for Android and any other platform on which it is published; “we”, “us” and “the developer” mean the individual developer of the App; “you” means the person using the App; “Your Server” means the Nextcloud instance whose address you enter into the App, whether operated by you or by another party of your choosing; and “Personal Data” has the meaning given in applicable data-protection law, including Regulation (EU) 2016/679 (GDPR), the UK GDPR and the California Consumer Privacy Act, to the extent they apply.

      + +

      2. We do not collect your data

      +

      The developer does not collect, receive, record, store, transmit, process, sell, rent, license, disclose or otherwise have access to any Personal Data or any other information about you or your use of the App. The App contains no code that sends information to the developer or to any server operated by the developer. Because we hold no such data, we are not a controller or a processor of your Personal Data in respect of the App, and there is no data we are able to disclose, delete, correct or hand over to any third party, including in response to a legal request.

      +

      Specifically, the App does not contain or use:

      +
        +
      • analytics, telemetry, usage statistics or behavioural measurement of any kind;
      • +
      • advertising networks, advertising identifiers or attribution services;
      • +
      • crash-reporting or error-reporting services;
      • +
      • social-media, marketing, A/B-testing or “growth” SDKs;
      • +
      • fingerprinting, cross-app tracking or profiling;
      • +
      • in-app purchases, subscriptions or accounts with the developer.
      • +
      + +

      3. Your Nextcloud server

      +

      The App is a client. Its only network communication is with Your Server, at the address you provide, in order to perform the actions you ask for: signing in, listing and transferring files, showing activity, managing shares and trash, and synchronising files you have chosen to keep available offline. This communication uses the standard Nextcloud and WebDAV interfaces and goes directly from your device to Your Server; it does not pass through the developer or any intermediary operated by the developer.

      +

      What Your Server logs, stores and does with that traffic is governed by the privacy policy and practices of whoever operates Your Server. We have no control over, and accept no responsibility for, those practices. If you use a server run by someone else, please consult them.

      +

      The App permits unencrypted (HTTP) connections so that you can reach servers on a private network. Whether to use HTTP or HTTPS is your choice; we strongly recommend HTTPS for any server reachable over the internet, because unencrypted traffic can be read by others on the network.

      +

      If you add several accounts, the App communicates with each account’s server only when you use that account, and never shares information between them.

      + +

      4. No third-party services

      +

      The App does not connect to any service other than Your Server. In particular:

      +
        +
      • Fonts and assets. The typefaces used by the App are bundled inside the App package. The App does not download fonts, images, icons, configuration or code from any third-party host at runtime.
      • +
      • No remote configuration. The App does not fetch feature flags, update manifests or any other instructions from the developer.
      • +
      • No embedded web content from others. Sign-in uses your device’s browser to open the login page of Your Server (see section 6); no third-party web content is loaded.
      • +
      • Open-source libraries. The App is built with open-source libraries that run locally on your device. They are not permitted by us to, and to our knowledge do not, transmit data to their authors.
      • +
      +

      If the App is distributed through an app store (for example Google Play), the store operator may collect information about your download and installation under its own policy. That collection is independent of the App and outside our control.

      + +

      5. Data stored on your device

      +

      To work, the App keeps some information locally on your device. This information remains on your device, is not transmitted to the developer, and is removed when you remove the account, clear the App’s storage or uninstall the App.

      + + + + + + + + +
      DataPurposeWhere it lives
      Server address, username and an app-specific access token for each accountKeep you signed inThe device’s secure credential storage (Android Keystore-backed storage)
      Preferences: theme, accent colour, bottom bar, tab order, action bar, swipe actions, lock and sync settingsRemember how you set the App upApp-private storage
      Cached file lists, thumbnails and previewsSpeed and offline browsingApp-private cache
      Files you marked for offline use and files you downloadOffline access; saving to your deviceApp-private storage, or your device’s Downloads folder when you save a file there
      +

      The App does not read your contacts, location, calendar, messages, call history, microphone or camera, and it does not scan or index your device’s other files. It reads a local file only when you pick it to upload or share it to the App.

      + +

      6. Authentication and app lock

      +

      Sign-in

      +

      Noo signs in using Nextcloud’s Login Flow v2. You enter only a server address; you authenticate on Your Server’s own web page in your browser, and Your Server issues the App an app-specific token. The App never sees, collects or stores your account password, and you can revoke the token at any time from the security settings of Your Server.

      +

      Biometric and device lock

      +

      The optional login lock, hidden-file lock and account-switch lock use your device’s own authentication (fingerprint, face, PIN, pattern or password) through the operating system. The operating system performs the check and tells the App only whether it succeeded. The App does not receive, store or transmit your biometric data, your PIN or your device credentials.

      + +

      7. Permissions

      +

      On Android the App requests only the permissions needed for the features you use:

      + + + + + + + + + +
      PermissionWhy
      InternetTo communicate with Your Server.
      Network stateTo know whether you are offline, so the App can avoid pointless requests and show offline files.
      BiometricTo support the optional device-based lock.
      Foreground service (data sync) and NotificationsTo keep an upload, download or sync running, and to show its progress with a cancel option, when the App is not on screen.
      Write external storage (Android 9 and earlier only)To save downloads on old devices that require it.
      +

      You can withhold or revoke any permission in your system settings; features that depend on it may stop working.

      + +

      8. Sharing and file handoff

      +

      When you use Noo’s sharing features, such as creating a share link, adding a person to a share, or sending a file directly, that action is carried out on Your Server. Anyone you share with can then access the content under Your Server’s rules. When you hand a file to another app through the system share sheet or file picker, that other app receives the file and is governed by its own privacy policy. You are responsible for what you choose to share and with whom.

      + +

      9. This website

      +

      This website (noo.ayushya.dev) is separate from the App. It is a static site: it has no accounts, forms, cookies, analytics or advertising, and it does not set tracking identifiers. The interactive demos run entirely in your browser and send nothing anywhere; any preferences you toggle in them are not stored.

      +

      To display its typefaces, the website loads fonts from Google Fonts (fonts.googleapis.com and fonts.gstatic.com). Your browser therefore sends your IP address and standard request headers to Google when you visit, subject to Google’s own privacy policy. The site is hosted on Cloudflare, which, like any web host, processes technical request data (such as IP address and user agent) to deliver pages and protect against abuse, under its own policy. We do not receive this data in any identifiable form beyond what the host’s standard dashboard may aggregate. None of this applies to the App.

      + +

      10. Children

      +

      The App is a general-purpose file client and is not directed at children. Because we collect no Personal Data from anyone, we also collect none from children. Accounts on Your Server, and any age requirements that apply to them, are determined by whoever operates Your Server.

      + +

      11. Your rights

      +

      Depending on where you live, you may have rights over your Personal Data, such as access, correction, deletion, portability, restriction and objection, and the right to complain to a supervisory authority. Since we hold no Personal Data about you, there is nothing for us to provide or erase; to exercise these rights over data held on Your Server, contact whoever operates it. You are always in control of data on your own device: you can delete any of it by removing an account, clearing the App’s storage, or uninstalling the App.

      +

      We do not sell or share Personal Data, as those terms are defined by the California Consumer Privacy Act, and we do not engage in targeted advertising or automated decision-making.

      + +

      12. Changes to this policy

      +

      If the App changes in a way that affects this policy, for example if it ever needed to connect to a service other than Your Server, we will update this page and its effective date before that change is released, and we will describe the change in the App’s release notes. Continued use of the App after an update means you accept the revised policy. Previous versions are available on request.

      + +

      13. Contact

      +

      Questions about this policy can be sent to the developer through the contact details published at ayushya.dev.

      + +

      Noo is an independent, unofficial client and is not affiliated with or endorsed by Nextcloud GmbH. Nextcloud is a trademark of Nextcloud GmbH.

      +
      +
      + + + + + + diff --git a/noo/script.js b/noo/script.js index f368d9a..6a7e7cb 100644 --- a/noo/script.js +++ b/noo/script.js @@ -1,141 +1,790 @@ -(() => { - "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")); - } -})(); +(() => { + "use strict"; + + const $ = (s, r = document) => r.querySelector(s); + const $$ = (s, r = document) => Array.from(r.querySelectorAll(s)); + const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + const darkQuery = window.matchMedia("(prefers-color-scheme: dark)"); + + /* Lucide icons, drawn from icons.svg (generated from the app's own design + kit) so every glyph in a mockup is the glyph the app ships. */ + const ic = (name, size = 20, cls = "") => + ``; + + const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); + + /* --------------------------------------------------------------------- + Tokens - lib/theme/design_tokens.dart, light + dark, plus the accent + presets from AppTheme.seedColors with the exact tonal values + NooColors.fromSeed produces for each. + --------------------------------------------------------------------- */ + + const PALETTE = { + light: { + bg: "#F3EFE6", sf: "#FFFFFF", sf2: "#F6F5F2", sf3: "#EAE8E2", ln: "#EAE8E2", + fg1: "#0E0D0B", fg2: "#5A5852", fg3: "#7C7A72", + dg: "#A8202A", dgs: "#FDE7E7", dgf: "#A8202A", + ok: "#3E7A23", oks: "#EAF5DF", wn: "#8C5A0E", wns: "#FBEFD0", in: "#2940A8", ins: "#E6ECFE", + scrim: "rgba(14,13,11,0.45)", + }, + dark: { + bg: "#0E0D0B", sf: "#25241F", sf2: "#3D3C38", sf3: "#5A5852", ln: "#3D3C38", + fg1: "#F3EFE6", fg2: "#BDBAB1", fg3: "#9E9B92", + dg: "#F49E9E", dgs: "#560F18", dgf: "#A8202A", + ok: "#B7DD9F", oks: "#1E3F10", wn: "#F0C26C", wns: "#4A3208", in: "#B6C3FB", ins: "#1C2B73", + scrim: "rgba(0,0,0,0.6)", + }, + }; + + const ACCENTS = [ + { id: "violet", name: "Default", c: "#8D0DE3", light: ["#8D0DE3", "#ECDFFF"], dark: ["#CBA5FD", "#2F0454"] }, + { id: "blue", name: "Nextcloud Blue", c: "#0082C9", light: ["#0082C9", "#CEE5FF"], dark: ["#9BCBFB", "#0D4A73"] }, + { id: "teal", name: "Ocean Teal", c: "#009688", light: ["#009688", "#9EF2E4"], dark: ["#82D5C8", "#005048"] }, + { id: "purple", name: "Deep Purple", c: "#6750A4", light: ["#6750A4", "#E9DDFF"], dark: ["#CFBDFE", "#4D3D75"] }, + { id: "amber", name: "Sunset Amber", c: "#E65100", light: ["#E65100", "#FFDBCF"], dark: ["#FFB59A", "#71361E"] }, + { id: "green", name: "Emerald Green", c: "#2E7D32", light: ["#2E7D32", "#BCF0B4"], dark: ["#A1D39A", "#245024"] }, + // Material You: follows the OS wallpaper, so this is only a sample. + { id: "wallpaper", name: "Match wallpaper", c: "#8E4956", light: ["#8E4956", "#FFD9DE"], dark: ["#FFB1BD", "#72333F"], wallpaper: true }, + ]; + + function resolveVars(s) { + const mode = s.theme === "system" ? (darkQuery.matches ? "dark" : "light") : s.theme; + const p = { ...PALETTE[mode] }; + const a = ACCENTS.find((x) => x.id === s.accent) || ACCENTS[0]; + const [text, soft] = a[mode]; + if (mode === "dark" && s.amoled) p.bg = p.sf = p.sf2 = p.sf3 = "#000000"; + const v = { ...p, ac: a.c, act: text, acs: soft }; + return { mode, css: Object.entries(v).map(([k, val]) => `--${k}:${val}`).join(";") }; + } + + /* --------------------------------------------------------------------- + Mock content + --------------------------------------------------------------------- */ + + const TABS = { + files: { label: "Files", icon: "folder", blurb: "Browse, upload, sort and filter every folder." }, + photos: { label: "Photos", icon: "photos", blurb: "Every image and video, grouped by month." }, + favorites: { label: "Favorites", icon: "star", blurb: "Everything you've starred, in one list." }, + activity: { label: "Activity", icon: "activity", blurb: "What changed and who changed it." }, + trash: { label: "Trash", icon: "trash", blurb: "Restore or permanently delete removed files." }, + shares: { label: "Shares", icon: "share", blurb: "What you've shared and what's shared with you." }, + recent: { label: "Recent", icon: "clock", blurb: "Recently edited and opened, by day." }, + offline: { label: "Offline", icon: "offline", blurb: "Synced folders and files, available without a connection." }, + }; + const TAB_ORDER_DEFAULT = ["files", "photos", "favorites", "offline", "shares", "recent", "activity", "trash"]; + const PINNED_DEFAULT = ["files", "photos", "favorites", "offline", "shares"]; + + const KIND_ICON = { folder: "folder", pdf: "doc", doc: "doc", sheet: "sheet", video: "film", image: "image", zip: "zip" }; + const KIND_VARS = { + folder: ["acs", "act"], pdf: ["dgs", "dg"], doc: ["ins", "in"], image: ["ins", "in"], + sheet: ["oks", "ok"], video: ["wns", "wn"], zip: ["sf3", "fg2"], + }; + + // [name, kind, meta, flags] + const ROWS = { + files: [ + ["Documents", "folder", "24 items · Sep 20"], + ["Photos", "folder", "1,204 items · Today"], + ["Projects", "folder", "8 items · Sep 19", "shared"], + ["Q3 Planning.pdf", "pdf", "2.4 MB · Yesterday", "fav ok"], + ["Budget 2026.xlsx", "sheet", "186 KB · Sep 18", "ok"], + ["Offsite recap.mp4", "video", "412 MB · Sep 14"], + ["Whiteboard.jpg", "image", "3.1 MB · Sep 12"], + ["Brand brief.docx", "doc", "58 KB · Sep 10"], + ], + favorites: [ + ["Q3 Planning.pdf", "pdf", "2.4 MB · Yesterday", "ok"], + ["Projects", "folder", "8 items · Sep 19", "shared"], + ["Passport scan.pdf", "pdf", "1.1 MB · Aug 30", "ok"], + ["Family budget.xlsx", "sheet", "92 KB · Aug 21", "shared"], + ["Wedding", "folder", "212 items · Jun 14"], + ], + offline: [ + ["Q3 Planning.pdf", "pdf", "Up to date · 2.4 MB", "ok"], + ["Offsite recap.mp4", "video", "Up to date · 412 MB", "ok"], + ["Contracts", "folder", "Syncing 12 of 18 files", "sync"], + ["Passport scan.pdf", "pdf", "Up to date · 1.1 MB", "ok"], + ], + shares: [ + ["Projects", "folder", "Ana Souza · Can edit", "shared"], + ["Offsite photos", "folder", "Leo Park · Can view", "shared"], + ["Q3 Planning.pdf", "pdf", "Ana Souza · Can edit", "shared"], + ], + recent: [ + ["Q3 Planning.pdf", "pdf", "Edited 10:24 · Documents", "fav"], + ["Whiteboard.jpg", "image", "Uploaded 9:40 · Photos"], + ["Brand brief.docx", "doc", "Opened 8:12 · Projects"], + ["Budget 2026.xlsx", "sheet", "Edited yesterday · Documents"], + ], + trash: [ + ["Old notes.txt", "doc", "Deleted today · /Documents"], + ["Draft v1.docx", "doc", "Deleted yesterday · /Projects"], + ["Screenshots", "folder", "Deleted Sep 12 · /Photos"], + ], + activity: [ + ["Ana Souza shared Q3 Planning.pdf with you", "pdf", "10:24"], + ["Leo Park edited Budget 2026.xlsx", "sheet", "9:02"], + ["You uploaded Whiteboard.jpg", "image", "Yesterday"], + ["Ana Souza commented on Projects", "folder", "Yesterday"], + ], + }; + const PHOTO_TINTS = ["#8DCDE2", "#96B0FD", "#E5D8BD", "#EEEDB3", "#DA9AC7", "#A7D296", "#E9A6A7", "#85D1BD", "#CBA5FD"]; + + const avatar = (txt, tint, size = 36, current = false) => + `${esc(txt)}`; + + const fileRow = (r) => { + const [name, kind, meta, flags = ""] = r; + const [bg, fg] = KIND_VARS[kind]; + const f = flags.split(" "); + const badges = + (f.includes("fav") ? ic("star", 16, "b-fav") : "") + + (f.includes("shared") ? ic("users", 16, "b-muted") : "") + + (f.includes("ok") ? ic("ok", 16, "b-ok") : "") + + (f.includes("sync") ? ic("sync", 16, "b-sync") : ""); + return `
      ${ic(KIND_ICON[kind], 20)}` + + `${esc(name)}${esc(meta)}${badges}
      `; + }; + + /* --------------------------------------------------------------------- + The phone: top bar, body, bottom bar (attached / floating / search + satellite), drawer + avatar menu. Mirrors NooTopBar, NooBottomBar, + NooDrawer and the avatar menu in the Flutter app (Android layout). + --------------------------------------------------------------------- */ + + function phoneState(over = {}) { + return { + theme: "light", accent: "violet", amoled: false, + bar: "floating", searchInBar: false, nav: "drawer", + order: [...TAB_ORDER_DEFAULT], pinned: new Set(PINNED_DEFAULT), + active: "files", overlay: null, ...over, + }; + } + + function visibleTabs(s) { + const max = s.searchInBar ? 4 : 5; + return s.order.filter((t) => s.pinned.has(t)).slice(0, max); + } + + function renderPhone(host, s) { + const { css } = resolveVars(s); + const tabs = visibleTabs(s); + if (!tabs.includes(s.active)) s.active = tabs[0] || "files"; + const hidden = s.order.filter((t) => !tabs.includes(t)); + const t = TABS[s.active]; + const floating = s.bar === "floating"; + + const bodyRows = + s.active === "photos" + ? `
      ${PHOTO_TINTS.concat(PHOTO_TINTS).slice(0, 12) + .map((c, i) => ``).join("")}
      ` + : `
      ${(ROWS[s.active] || ROWS.files).map(fileRow).join("")}
      `; + + const searchLauncher = !s.searchInBar + ? `${ic("search", 18)}Search` + : ""; + + const hamburger = s.nav === "drawer" + ? `` + : ""; + + const barItem = (id) => { + const sel = id === s.active; + const d = TABS[id]; + if (floating && !sel) { + return ``; + } + return ``; + }; + const searchItem = !s.searchInBar ? "" : floating + ? `` + : ""; + const attachedSearch = s.searchInBar && !floating + ? `` + : ""; + + const bar = `
      ` + + `
      ${tabs.map(barItem).join("")}${attachedSearch}
      ${searchItem}
      `; + + const fab = s.active === "files" + ? `` + : ""; + + let overlay = ""; + if (s.overlay === "drawer") { + overlay = `
      `; + } else if (s.overlay === "avatar") { + overlay = `
      `; + } + + host.innerHTML = + `
      ` + + `
      9:41${ic("signal", 14)}${ic("wifi", 14)}${ic("battery", 16)}
      ` + + `
      ${hamburger}

      ${t.label}

      ${searchLauncher}` + + `
      ` + + `
      ${bodyRows}
      ${fab}${bar}${overlay}
      `; + } + + function mountPhone(host, state) { + const rerender = () => renderPhone(host, state); + host.addEventListener("click", (e) => { + const tabBtn = e.target.closest("[data-tab]"); + const act = e.target.closest("[data-act]"); + if (tabBtn) { + state.active = tabBtn.dataset.tab; + state.overlay = null; + rerender(); + } else if (act) { + const a = act.dataset.act; + if (a === "menu") state.overlay = "drawer"; + else if (a === "avatar") state.overlay = state.nav === "avatar" ? "avatar" : null; + else if (a === "close") state.overlay = null; + rerender(); + } + }); + rerender(); + return rerender; + } + + /* --------------------------------------------------------------------- + Hero phone + --------------------------------------------------------------------- */ + + const heroPhone = $("#heroPhone"); + if (heroPhone) { + const heroState = phoneState({ theme: "system", bar: "floating" }); + const r = mountPhone(heroPhone, heroState); + darkQuery.addEventListener?.("change", r); + } + + /* --------------------------------------------------------------------- + Appearance lab + --------------------------------------------------------------------- */ + + const lab = $("#labPhone"); + if (lab) { + const S = phoneState({ theme: "system" }); + const rerender = mountPhone(lab, S); + darkQuery.addEventListener?.("change", rerender); + + // Accent swatches + const sw = $("#labAccents"); + sw.innerHTML = ACCENTS.map((a) => + `` + ).join(""); + const accName = $("#labAccentName"); + const syncAccent = () => { + $$(".swatch-btn", sw).forEach((b) => b.classList.toggle("is-active", b.dataset.accent === S.accent)); + const a = ACCENTS.find((x) => x.id === S.accent); + accName.textContent = a.wallpaper ? "Match wallpaper (sample colour shown)" : a.name; + }; + sw.addEventListener("click", (e) => { + const b = e.target.closest("[data-accent]"); + if (!b) return; + S.accent = b.dataset.accent; + syncAccent(); + rerender(); + }); + syncAccent(); + + // Segmented controls: data-lab="theme|bar|nav" data-val="..." + $$("[data-lab]").forEach((group) => { + const key = group.dataset.lab; + const sync = () => $$("[data-val]", group).forEach((b) => b.classList.toggle("is-active", String(S[key]) === b.dataset.val)); + group.addEventListener("click", (e) => { + const b = e.target.closest("[data-val]"); + if (!b) return; + S[key] = b.dataset.val; + S.overlay = null; + sync(); + rerender(); + updateAmoled(); + tabsRender(); + }); + sync(); + }); + + // Toggles: data-toggle="amoled|searchInBar" + const amoledRow = $("#amoledRow"); + const resolvedMode = () => resolveVars(S).mode; + function updateAmoled() { + const isDark = resolvedMode() === "dark"; + amoledRow.classList.toggle("is-disabled", !isDark); + $("[data-toggle=amoled]").disabled = !isDark; + } + $$("[data-toggle]").forEach((t) => { + const key = t.dataset.toggle; + t.setAttribute("role", "switch"); + const sync = () => { + t.classList.toggle("is-on", !!S[key]); + t.setAttribute("aria-checked", String(!!S[key])); + }; + t.addEventListener("click", () => { + S[key] = !S[key]; + sync(); + rerender(); + tabsRender(); + }); + sync(); + }); + darkQuery.addEventListener?.("change", updateAmoled); + updateAmoled(); + + // Tabs: reorder + pin + const tabsList = $("#labTabs"); + const tabsNote = $("#labTabsNote"); + function tabsRender() { + const max = S.searchInBar ? 4 : 5; + tabsNote.textContent = `Drag to reorder, pin up to ${max} to the bottom bar` + (S.searchInBar ? " (Search takes the fifth slot)" : ""); + tabsList.innerHTML = S.order.map((id) => + `
    • ${ic("grip", 18)}` + + `${ic(TABS[id].icon, 20)}${TABS[id].label}` + + `
    • ` + ).join(""); + } + tabsList.addEventListener("click", (e) => { + const b = e.target.closest("[data-pin]"); + if (!b) return; + const id = b.dataset.pin; + const max = S.searchInBar ? 4 : 5; + if (S.pinned.has(id)) { + if (S.pinned.size <= 1) return; + S.pinned.delete(id); + } else { + if (S.pinned.size >= max) { + tabsNote.textContent = `The bottom bar holds ${max} tabs - unpin one first`; + return; + } + S.pinned.add(id); + } + S.overlay = null; + tabsRender(); + rerender(); + }); + sortable(tabsList, (ids) => { + S.order = ids; + S.overlay = null; + rerender(); + }); + tabsRender(); + } + + /* Pointer-driven list reorder: the dragged row moves through the DOM and + the new order is read back on release. */ + function sortable(list, onChange) { + list.addEventListener("pointerdown", (e) => { + const grip = e.target.closest("[data-grip]"); + if (!grip) return; + const row = grip.closest("[data-id]"); + e.preventDefault(); + row.classList.add("is-dragging"); + grip.setPointerCapture(e.pointerId); + const move = (ev) => { + const siblings = $$("[data-id]", list).filter((r) => r !== row); + const next = siblings.find((r) => { + const b = r.getBoundingClientRect(); + return ev.clientY < b.top + b.height / 2; + }); + if (next) list.insertBefore(row, next); + else list.appendChild(row); + }; + const up = () => { + grip.removeEventListener("pointermove", move); + grip.removeEventListener("pointerup", up); + grip.removeEventListener("pointercancel", up); + row.classList.remove("is-dragging"); + onChange($$("[data-id]", list).map((r) => r.dataset.id)); + }; + grip.addEventListener("pointermove", move); + grip.addEventListener("pointerup", up); + grip.addEventListener("pointercancel", up); + }); + } + + /* --------------------------------------------------------------------- + Accounts + --------------------------------------------------------------------- */ + + const acctHost = $("#acctDemo"); + if (acctHost) { + const accounts = [ + { u: "ayushya", h: "cloud.example.com", t: "#DA9AC7", l: "A" }, + { u: "ayushya.dev", h: "drive.ayushya.dev", t: "#8DCDE2", l: "D" }, + { u: "family", h: "nc.home.lan", t: "#A7D296", l: "F" }, + ]; + let current = 0; + const draw = () => { + acctHost.innerHTML = + accounts.map((a, i) => + `` + ).join("") + + `
      ${ic("userPlus", 20)}Add account
      `; + }; + acctHost.addEventListener("click", (e) => { + const b = e.target.closest("[data-acct]"); + if (!b) return; + current = Number(b.dataset.acct); + draw(); + const note = $("#acctNote"); + if (note) note.textContent = `Now browsing ${accounts[current].h}. Each account keeps its own sync, cache and favorites.`; + }); + draw(); + } + + /* --------------------------------------------------------------------- + Locks + --------------------------------------------------------------------- */ + + const lockHost = $("#lockDemo"); + if (lockHost) { + const L = { login: true, switching: true, hidden: false }; + const rows = [ + ["login", "lock", "Lock with fingerprint", "Require this device's PIN or biometric to open Noo", false], + ["switching", "switch", "Lock account switching", "Unlock to switch between saved accounts", true], + ["hidden", "hidden", "Lock hidden files", "Unlock to reveal hidden files and folders", true], + ]; + const draw = () => { + lockHost.innerHTML = rows.map(([k, icon, label, sub, dep]) => { + const off = dep && !L.login; + return `
      ${ic(icon, 20)}` + + `${label}${sub}` + + `
      `; + }).join(""); + }; + lockHost.addEventListener("click", (e) => { + const b = e.target.closest("[data-lock]"); + if (!b || b.disabled) return; + L[b.dataset.lock] = !L[b.dataset.lock]; + draw(); + }); + draw(); + } + + /* --------------------------------------------------------------------- + Sync + --------------------------------------------------------------------- */ + + const syncHost = $("#syncDemo"); + if (syncHost) { + const Y = { all: false, notif: true, cell: false, busy: false, done: false }; + const draw = () => { + const tog = (k, on) => ``; + syncHost.innerHTML = + `
      ${ic("cloud", 20)}Sync everythingMirror the whole account instead of picking folders${tog("all", Y.all)}
      ` + + `
      ${ic("offline", 20)}View offline files${Y.all ? "Every folder in this account is synced" : "3 synced folders/files"}${ic("chevR", 18, "b-muted")}
      ` + + `
      ${ic("bell", 20)}Background sync notificationsShow a notification when a background sync updates files. Conflicts and "Sync now" always notify.${tog("notif", Y.notif)}
      ` + + `
      ${ic("signal", 20)}Sync on cellularOff = background sync only runs on Wi-Fi${tog("cell", Y.cell)}
      ` + + `
      ${ic("refresh", 20)}Sync now${Y.done ? `Up to date - just now` : ""}` + + (Y.busy ? `` : ``) + `
      `; + }; + syncHost.addEventListener("click", (e) => { + const b = e.target.closest("[data-sync]"); + if (!b) return; + const k = b.dataset.sync; + if (k === "now") { + Y.busy = true; Y.done = false; draw(); + setTimeout(() => { Y.busy = false; Y.done = true; draw(); }, 1400); + } else { + Y[k] = !Y[k]; + draw(); + } + }); + draw(); + } + + /* --------------------------------------------------------------------- + Share sheet + --------------------------------------------------------------------- */ + + const shareHost = $("#shareDemo"); + if (shareHost) { + const people = [ + { n: "ayushya", sub: "cloud.example.com", t: "#DA9AC7", l: "A", owner: true }, + { n: "Ana Souza", sub: "ana@example.com", t: "#8DCDE2", l: "AS", perm: "Can edit" }, + { n: "Leo Park", sub: "Design team", t: "#A7D296", l: "LP", perm: "Can view" }, + ]; + const SH = { link: true, linkPerm: "Can view", copied: false, menu: null, query: "" }; + const draw = () => { + shareHost.innerHTML = + `
      Q3 Planning.pdf2.4 MB
      ` + + `
      Share with people
      ` + + `` + + people.map((p, i) => + `
      ${avatar(p.l, p.t, 36)}${p.n}${p.sub}` + + (p.owner ? `Owner` : `` + + (SH.menu === i ? `${["Can view", "Can edit"].map((o) => ``).join("")}` : "") + ``) + `
      ` + ).join("") + `
      ` + + `
      Share link
      ` + + (SH.link + ? `
      ${ic("link", 20)}Public linkcloud.example.com/s/q3-plan
      ` + + `` + : `

      Turn on to create a public link.

      `) + `
      ` + + `
      Send file directly
      ` + + `

      Link settings above don't apply - this sends the file's bytes directly.

      `; + }; + shareHost.addEventListener("click", (e) => { + const t = e.target; + let b; + if ((b = t.closest("[data-perm]"))) { + const i = Number(b.dataset.perm); + SH.menu = SH.menu === i ? null : i; + } else if ((b = t.closest("[data-set]"))) { + const [i, v] = b.dataset.set.split("|"); + if (v === "remove") people.splice(Number(i), 1); + else people[Number(i)].perm = v; + SH.menu = null; + } else if (t.closest("[data-link]")) { + SH.link = !SH.link; + SH.copied = false; + } else if (t.closest("[data-linkperm]")) { + SH.linkPerm = SH.linkPerm === "Can view" ? "Can edit" : "Can view"; + } else if (t.closest("[data-copy]")) { + SH.copied = true; + setTimeout(() => { SH.copied = false; draw(); }, 1600); + } else if (!t.closest(".mini-menu")) { + if (SH.menu === null) return; + SH.menu = null; + } else return; + draw(); + }); + shareHost.addEventListener("input", (e) => { SH.query = e.target.value; }); + draw(); + } + + /* --------------------------------------------------------------------- + Action bar + --------------------------------------------------------------------- */ + + const ACTIONS = { + favorite: ["Favorite", "star"], share: ["Share", "share"], download: ["Download", "download"], + delete: ["Delete", "trash"], copy: ["Copy", "copy"], move: ["Move", "move"], + rename: ["Rename", "rename"], sync: ["Sync to device", "offline"], details: ["Details", "info"], + }; + + const actionHost = $("#actionDemo"); + if (actionHost) { + let order = Object.keys(ACTIONS); + let more = false; + const INLINE = 3; + const orderList = $("#actionOrder"); + const bar = $("#actionBar"); + + const drawBar = () => { + const inline = order.slice(0, INLINE); + const rest = order.slice(INLINE); + bar.innerHTML = + `
      ${ic("x", 18)}2 selected` + + `${inline.map((k) => ``).join("")}` + + `
      ` + + (more ? `` : ""); + }; + const drawList = () => { + orderList.innerHTML = order.map((k, i) => + `
    • ${ic("grip", 18)}` + + `${ic(ACTIONS[k][1], 20)}${ACTIONS[k][0]}` + + `${i < INLINE ? "In bar" : "More"}
    • ` + ).join(""); + }; + sortable(orderList, (ids) => { order = ids; drawList(); drawBar(); }); + bar.addEventListener("click", (e) => { + if (e.target.closest("[data-more]")) { more = !more; drawBar(); } + }); + drawList(); + drawBar(); + } + + /* --------------------------------------------------------------------- + Swiping + --------------------------------------------------------------------- */ + + const swipeHost = $("#swipeDemo"); + if (swipeHost) { + const BLOCK = 96, THROUGH = 1.8; + const cfg = { right: "favorite", left: "delete" }; + const files = [ROWS.files[3], ROWS.files[4], ROWS.files[6], ROWS.files[7]].map((r) => ({ r, fav: r[3] && r[3].includes("fav") })); + const toast = $("#swipeToast"); + let toastTimer; + const say = (msg) => { + toast.textContent = msg; + toast.classList.add("is-on"); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => toast.classList.remove("is-on"), 1800); + }; + const meta = { + none: null, + favorite: ["Favorite", "star", "ac"], + delete: ["Delete", "trash", "dgf"], + share: ["Share", "share", "ac"], + }; + const list = $("#swipeList"); + + const drawRows = () => { + list.innerHTML = files.map((f, i) => { + const row = fileRow([f.r[0], f.r[1], f.r[2], (f.fav ? "fav " : "") + (f.r[3] || "").replace("fav", "")]); + const blk = (side) => { + const m = meta[cfg[side]]; + if (!m) return ""; + return `
      ${ic(m[1], 20)}${f.fav && cfg[side] === "favorite" ? "Unfavorite" : m[0]}
      `; + }; + return `
      ${blk("right")}${blk("left")}
      ${row}
      `; + }).join(""); + }; + + const trigger = (i, side) => { + const a = cfg[side]; + const f = files[i]; + if (a === "favorite") { f.fav = !f.fav; say(f.fav ? `Added ${f.r[0]} to favorites` : `Removed ${f.r[0]} from favorites`); drawRows(); } + else if (a === "delete") { + const el = $(`.sw[data-i="${i}"]`, list); + el.classList.add("is-gone"); + say(`Moved ${f.r[0]} to trash (demo - it comes back)`); + setTimeout(drawRows, 1500); + } else if (a === "share") { say(`Share sheet opens for ${f.r[0]}`); drawRows(); } + }; + + let drag = null; + list.addEventListener("pointerdown", (e) => { + const sw = e.target.closest(".sw"); + if (!sw) return; + const blk = e.target.closest("[data-act-side]"); + if (blk && sw.dataset.open === blk.dataset.actSide) { trigger(Number(sw.dataset.i), blk.dataset.actSide); return; } + const fg = $(".sw-fg", sw); + const base = Number(sw.dataset.dx || 0); + drag = { sw, fg, x0: e.clientX, base, moved: false, id: e.pointerId }; + sw.setPointerCapture?.(e.pointerId); + }); + list.addEventListener("pointermove", (e) => { + if (!drag || e.pointerId !== drag.id) return; + let dx = drag.base + (e.clientX - drag.x0); + if (Math.abs(e.clientX - drag.x0) > 4) drag.moved = true; + const maxRight = cfg.right === "none" ? 0 : BLOCK * 2.2; + const maxLeft = cfg.left === "none" ? 0 : BLOCK * 2.2; + dx = Math.max(-maxLeft, Math.min(maxRight, dx)); + drag.dx = dx; + drag.sw.dataset.dx = dx; + drag.sw.classList.add("is-dragging"); + drag.sw.classList.toggle("show-right", dx > 0); + drag.sw.classList.toggle("show-left", dx < 0); + drag.sw.style.setProperty("--dx", dx + "px"); + drag.sw.classList.toggle("is-through", Math.abs(dx) >= BLOCK * THROUGH); + }); + const end = (e) => { + if (!drag || e.pointerId !== drag.id) return; + const { sw } = drag; + const dx = drag.dx ?? drag.base; + const side = dx > 0 ? "right" : "left"; + sw.classList.remove("is-dragging", "is-through"); + let target = 0; + if (Math.abs(dx) >= BLOCK * THROUGH) { + trigger(Number(sw.dataset.i), side); + drag = null; + return; + } + if (Math.abs(dx) > BLOCK / 2) target = Math.sign(dx) * BLOCK; + sw.dataset.dx = target; + sw.dataset.open = target === 0 ? "" : side; + sw.style.setProperty("--dx", target + "px"); + if (target === 0) sw.classList.remove("show-right", "show-left"); + drag = null; + }; + list.addEventListener("pointerup", end); + list.addEventListener("pointercancel", end); + + // Pickers + $$("[data-swipe]", swipeHost).forEach((group) => { + const side = group.dataset.swipe; + const sync = () => $$("[data-val]", group).forEach((b) => b.classList.toggle("is-active", b.dataset.val === cfg[side])); + group.addEventListener("click", (e) => { + const b = e.target.closest("[data-val]"); + if (!b) return; + cfg[side] = b.dataset.val; + sync(); + drawRows(); + }); + sync(); + }); + drawRows(); + } + + /* --------------------------------------------------------------------- + Page chrome: mobile nav, hero parallax, scroll reveal + --------------------------------------------------------------------- */ + + const navToggle = $("#navToggle"); + const mobileNav = $("#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"); + }); + $$("a", mobileNav).forEach((a) => + a.addEventListener("click", () => { + mobileNav.classList.remove("is-open"); + navToggle.setAttribute("aria-expanded", "false"); + }) + ); + } + + const heroVisual = $("#heroVisual"); + if (heroVisual && !reduceMotion) { + const layers = $$("[data-depth]", heroVisual).map((el) => ({ el, depth: parseFloat(el.dataset.depth) || 0.3 })); + let scrollOffset = 0; + let pointer = { x: 0, y: 0 }; + let ticking = false; + + const apply = () => { + const rect = heroVisual.getBoundingClientRect(); + if (rect.bottom > 0 && rect.top < window.innerHeight) { + for (const l of layers) { + l.el.style.transform = `translate3d(${pointer.x * l.depth * 14}px, ${scrollOffset * l.depth * 40 + pointer.y * l.depth * 14}px, 0)`; + } + } + ticking = false; + }; + const tick = () => { if (!ticking) { ticking = true; requestAnimationFrame(apply); } }; + const onScroll = () => { + const rect = heroVisual.getBoundingClientRect(); + scrollOffset = (window.innerHeight / 2 - (rect.top + rect.height / 2)) / window.innerHeight; + tick(); + }; + window.addEventListener("scroll", onScroll, { passive: true }); + heroVisual.addEventListener("pointermove", (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; + tick(); + }); + heroVisual.addEventListener("pointerleave", () => { pointer = { x: 0, y: 0 }; tick(); }); + onScroll(); + } + + const revealTargets = $$(".card, .showcase-copy, .showcase-demo, .tab-chip, .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.12, rootMargin: "0px 0px -40px 0px" } + ); + revealTargets.forEach((el) => io.observe(el)); + } else { + revealTargets.forEach((el) => el.classList.add("is-visible")); + } +})(); diff --git a/noo/styles.css b/noo/styles.css index 9a642d9..aeea03f 100644 --- a/noo/styles.css +++ b/noo/styles.css @@ -1,21 +1,30 @@ /* Noo marketing site — tokens mirror lib/theme/design_tokens.dart exactly, - so this reads as the same product, not a separate marketing skin. */ + so this reads as the same product, not a separate marketing skin. The + short names (--sf, --act, --acs ...) match the ones the interactive + mockups use inside the phone, so a mock card and the page share one set. */ :root { --bg: #F3EFE6; - --surface: #FFFFFF; - --surface2: #F6F5F2; - --surface3: #EAE8E2; - --line: #EAE8E2; + --sf: #FFFFFF; + --sf2: #F6F5F2; + --sf3: #EAE8E2; + --ln: #EAE8E2; --fg1: #0E0D0B; --fg2: #5A5852; --fg3: #7C7A72; - --accent: #8D0DE3; - --accent-text: #8D0DE3; - --accent-soft: #ECDFFF; - --danger: #A8202A; - - --demo-accent: #8D0DE3; + --ac: #8D0DE3; + --act: #8D0DE3; + --acs: #ECDFFF; + --dg: #A8202A; + --dgs: #FDE7E7; + --dgf: #A8202A; + --ok: #3E7A23; + --oks: #EAF5DF; + --wn: #8C5A0E; + --wns: #FBEFD0; + --in: #2940A8; + --ins: #E6ECFE; + --shadow: 0 24px 48px rgba(30, 0, 47, 0.14); --font-display: 'Schibsted Grotesk', system-ui, sans-serif; --font-body: 'Instrument Sans', system-ui, sans-serif; @@ -37,41 +46,29 @@ @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; + --bg: #0E0D0B; --sf: #25241F; --sf2: #3D3C38; --sf3: #5A5852; --ln: #3D3C38; + --fg1: #F3EFE6; --fg2: #BDBAB1; --fg3: #9E9B92; + --act: #CBA5FD; --acs: #2F0454; + --dg: #F49E9E; --dgs: #560F18; + --ok: #B7DD9F; --oks: #1E3F10; --wn: #F0C26C; --wns: #4A3208; --in: #B6C3FB; --ins: #1C2B73; + --shadow: 0 24px 48px rgba(0, 0, 0, 0.5); 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; + --bg: #0E0D0B; --sf: #25241F; --sf2: #3D3C38; --sf3: #5A5852; --ln: #3D3C38; + --fg1: #F3EFE6; --fg2: #BDBAB1; --fg3: #9E9B92; + --act: #CBA5FD; --acs: #2F0454; + --dg: #F49E9E; --dgs: #560F18; + --ok: #B7DD9F; --oks: #1E3F10; --wn: #F0C26C; --wns: #4A3208; --in: #B6C3FB; --ins: #1C2B73; + --shadow: 0 24px 48px rgba(0, 0, 0, 0.5); color-scheme: dark; } * { box-sizing: border-box; } -html { scroll-behavior: smooth; } +html { scroll-behavior: smooth; scroll-padding-top: 84px; } body { margin: 0; @@ -81,19 +78,30 @@ body { font-size: 16px; line-height: 1.5; -webkit-font-smoothing: antialiased; + overflow-x: clip; } -img, svg { display: block; max-width: 100%; } - +img { display: block; max-width: 100%; } a { color: inherit; } +button { font-family: inherit; color: inherit; } -h1, h2, h3 { +h1, h2, h3, h4, h5 { font-family: var(--font-display); font-weight: 600; margin: 0; letter-spacing: -0.02em; } +/* Lucide glyphs from icons.svg */ +.i { + flex: none; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + .wrap { max-width: var(--content-max); margin: 0 auto; @@ -104,7 +112,7 @@ h1, h2, h3 { position: absolute; left: -9999px; top: 0; - background: var(--accent); + background: var(--ac); color: #fff; padding: 10px 16px; border-radius: var(--radius-input); @@ -113,15 +121,17 @@ h1, h2, h3 { .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); + color: var(--act); margin: 0 0 12px; } +.text-link { color: var(--act); font-weight: 600; text-decoration: none; } +.text-link:hover { text-decoration: underline; } + /* ---------- buttons ---------- */ .btn { @@ -139,16 +149,15 @@ h1, h2, h3 { white-space: nowrap; } .btn:active { transform: scale(0.98); } - -.btn-primary { background: var(--accent); color: #fff; } +.btn-primary { background: var(--ac); 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-outline { background: transparent; color: var(--fg1); border-color: var(--ln); } +.btn-outline:hover { background: var(--sf2); } .btn-lg { padding: 14px 26px; font-size: 16px; } .btn-sm { padding: 9px 18px; font-size: 14px; } +:focus-visible { outline: 2px solid var(--ac); outline-offset: 2px; } + /* ---------- header ---------- */ .site-header { @@ -156,50 +165,37 @@ h1, h2, h3 { top: 0; z-index: 40; background: var(--bg); - border-bottom: 1px solid var(--line); + border-bottom: 1px solid var(--ln); } -.header-row { - display: flex; - align-items: center; - gap: 24px; - height: 72px; -} +.header-row { display: flex; align-items: center; gap: 24px; height: 72px; } .wordmark { display: flex; align-items: center; - gap: 10px; + gap: 8px; font-family: var(--font-display); font-weight: 700; - font-size: 20px; + font-size: 22px; + letter-spacing: -0.03em; text-decoration: none; color: var(--fg1); margin-right: auto; } -.wordmark-sm { font-size: 17px; } +.wordmark-sm { font-size: 18px; } +/* The app's own monochrome mark (noo-mark.png), tinted with the accent. */ .wordmark-mark { - width: 26px; - height: 26px; - border-radius: 8px; - background: var(--accent); - position: relative; + width: 34px; + height: 34px; flex: none; + background: var(--ac); + -webkit-mask: url("noo-mark.png") center / 132% no-repeat; + mask: url("noo-mark.png") center / 132% no-repeat; } -.wordmark-mark::after { - content: ""; - position: absolute; - inset: 7px; - border-radius: 4px; - background: #fff; - opacity: 0.92; -} +.wordmark-sm .wordmark-mark { width: 28px; height: 28px; } -.site-nav { - display: flex; - gap: 28px; -} +.site-nav { display: flex; gap: 28px; } .site-nav a { font-size: 15px; font-weight: 500; @@ -237,14 +233,14 @@ h1, h2, h3 { flex-direction: column; gap: 4px; padding: 8px var(--gutter) 20px; - border-top: 1px solid var(--line); + border-top: 1px solid var(--ln); } .mobile-nav a { padding: 12px 4px; text-decoration: none; color: var(--fg1); font-weight: 500; - border-bottom: 1px solid var(--line); + border-bottom: 1px solid var(--ln); } .mobile-nav a:last-child { border-bottom: none; margin-top: 8px; text-align: center; } .mobile-nav.is-open { display: flex; } @@ -265,23 +261,10 @@ h1, h2, h3 { 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-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(--act); } +.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 { @@ -295,18 +278,13 @@ h1, h2, h3 { 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-proof li::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--ac); } /* ---------- hero exploded visual ---------- */ .hero-visual { position: relative; - height: clamp(360px, 46vw, 520px); + min-height: clamp(420px, 50vw, 560px); display: flex; align-items: center; justify-content: center; @@ -315,155 +293,63 @@ h1, h2, h3 { .glow { position: absolute; inset: -10%; - background: radial-gradient(closest-side, color-mix(in srgb, var(--accent) 16%, transparent), transparent 70%); + background: radial-gradient(closest-side, color-mix(in srgb, var(--ac) 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); -} +.phone-wrap { position: relative; zoom: 0.78; will-change: transform; } +.phone-wrap-lg { zoom: 1; } .chip { position: absolute; - background: var(--surface); - border: 1px solid var(--line); + background: var(--sf); + border: 1px solid var(--ln); border-radius: var(--radius-card); will-change: transform; + box-shadow: var(--shadow); + z-index: 2; } -.chip-photo { - top: 6%; right: 2%; - width: 92px; height: 92px; - padding: 8px; - transform: rotate(-6deg); -} +.chip-photo { top: 4%; right: 0; 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-photo-grid span { border-radius: 8px; } +.chip-photo-grid span:nth-child(1) { background: #8DCDE2; } +.chip-photo-grid span:nth-child(2) { background: #E5D8BD; } +.chip-photo-grid span:nth-child(3) { background: #DA9AC7; } +.chip-photo-grid span:nth-child(4) { background: #A7D296; } -.chip-row { - bottom: 20%; right: -2%; +.chip-share { + bottom: 22%; right: -2%; display: flex; align-items: center; gap: 10px; padding: 12px 16px; transform: rotate(4deg); - width: 160px; + color: var(--act); } +.chip-share span { display: flex; flex-direction: column; line-height: 1.2; color: var(--fg1); font-size: 13px; } +.chip-share b { font-weight: 600; } +.chip-share i { font-style: normal; color: var(--fg3); font-size: 12px; } -.chip-swatches { - bottom: 6%; left: 4%; - display: flex; gap: 8px; - padding: 12px; - transform: rotate(-4deg); +.chip-lock { + top: 16%; left: -2%; + display: flex; align-items: center; gap: 8px; + padding: 10px 16px; + border-radius: var(--radius-pill); + font-size: 13px; font-weight: 600; + transform: rotate(-3deg); + color: var(--act); } -.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-lock span { color: var(--fg1); } .chip-badge { - bottom: -2%; left: 30%; + bottom: -2%; left: 0; display: flex; align-items: center; gap: 8px; padding: 9px 14px; font-size: 13px; font-weight: 500; border-radius: var(--radius-pill); transform: rotate(-2deg); + color: var(--ok); } -.chip-badge .mini-ic { width: 14px; height: 14px; } @media (max-width: 980px) { .hero-grid { grid-template-columns: 1fr; text-align: center; } @@ -472,225 +358,420 @@ h1, h2, h3 { .hero-actions, .hero-proof { justify-content: center; } .hero-visual { order: 1; } } +@media (max-width: 480px) { + .chip-photo, .chip-share { display: none; } + .phone-wrap { zoom: 0.7; } + .phone-wrap-lg { zoom: 0.9; } +} /* ---------- sections ---------- */ .section { padding: clamp(56px, 8vw, 104px) 0; } -.section-alt { background: var(--surface); } +.section-alt { background: var(--sf); } -.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; +.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; } + +/* ---------- feature showcases ---------- */ + +.showcase { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: clamp(28px, 6vw, 72px); + align-items: center; + margin-top: clamp(48px, 7vw, 88px); } +.showcase-flip .showcase-copy { order: 2; } +.showcase-flip .showcase-demo { order: 1; } -.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 { +.feat-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; + background: var(--acs); + color: var(--act); } -.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; +.showcase-copy h3 { font-size: clamp(22px, 2.4vw, 28px); margin: 16px 0 12px; line-height: 1.15; } +.showcase-copy p { margin: 0 0 12px; color: var(--fg2); max-width: 48ch; } +.showcase-copy .demo-note { font-size: 14px; color: var(--fg3); } + +@media (max-width: 820px) { + .showcase { grid-template-columns: 1fr; } + .showcase-flip .showcase-copy { order: 1; } + .showcase-flip .showcase-demo { order: 2; } } -/* 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); } +/* ---------- mock cards (settings-style groups, sheets) ---------- */ -: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); +.mock-card { + background: var(--sf); + border: 1px solid var(--ln); border-radius: var(--radius-card); - padding: 24px; + padding: 20px; + box-shadow: var(--shadow); +} +.section-alt .mock-card { background: var(--bg); box-shadow: none; } +.section-alt .mock-card .srow, .section-alt .mock-card .tab-row { background: var(--sf); } +.mock-card h3 { font-size: 20px; margin-bottom: 8px; } +.mock-card > p { margin: 0 0 16px; color: var(--fg2); font-size: 14px; } +.mock-title { font-size: 15px; color: var(--fg3); font-weight: 600; margin: 0 0 12px 4px; } + +.mock-group { display: flex; flex-direction: column; gap: 1px; background: var(--ln); border-radius: var(--radius-card); overflow: hidden; } + +.srow { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + min-height: 52px; + padding: 10px 16px; + background: var(--sf2); + border: 0; + text-align: left; + font: inherit; + cursor: default; +} +button.srow { cursor: pointer; } +button.srow:hover { filter: brightness(0.98); } +.srow.is-disabled { opacity: 0.45; } +.srow-ic { display: inline-flex; color: var(--fg2); flex: none; } +.srow-accent { color: var(--act); } +.srow-accent .srow-main b { color: var(--act); } +.srow-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } +.srow-main b { font-size: 15px; font-weight: 500; line-height: 1.2; } +.srow-main i { font-style: normal; font-size: 13px; color: var(--fg3); line-height: 1.3; } + +.pill-tag { + font-size: 12px; font-weight: 600; + padding: 3px 10px; + border-radius: var(--radius-pill); + background: var(--acs); color: var(--act); +} + +/* NooToggle */ +.toggle { + flex: none; + width: 46px; height: 28px; + border-radius: var(--radius-pill); + border: 0; + background: var(--sf3); + position: relative; + cursor: pointer; + padding: 0; + transition: background var(--motion-base) var(--ease); +} +.toggle i { + position: absolute; top: 4px; left: 4px; + width: 20px; height: 20px; + border-radius: 50%; + background: #fff; + transition: transform var(--motion-base) var(--ease); +} +.toggle.is-on { background: var(--ac); } +.toggle.is-on i { transform: translateX(18px); } +.toggle:disabled { cursor: not-allowed; } +.np .toggle, .mock-group .toggle { pointer-events: auto; } + +.btn-tonal { + border: 0; + background: var(--acs); + color: var(--act); + font-weight: 600; + font-size: 13px; + padding: 0 16px; + height: 32px; + border-radius: var(--radius-pill); + cursor: pointer; + display: inline-flex; align-items: center; gap: 6px; +} +.btn-wide { width: 100%; justify-content: center; height: 40px; } +.btn-primary-sm { + border: 0; + background: var(--ac); + color: #fff; + font-weight: 600; + font-size: 14px; + height: 40px; + border-radius: var(--radius-pill); + cursor: pointer; + width: 100%; + display: inline-flex; align-items: center; justify-content: center; gap: 8px; + margin-top: 8px; +} + +.spinner { + width: 18px; height: 18px; + border-radius: 50%; + border: 2px solid var(--acs); + border-top-color: var(--act); + animation: spin 800ms linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } + +/* avatars */ +.av { + --s: 36px; + width: var(--s); height: var(--s); + flex: none; + border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + font-family: var(--font-display); + font-weight: 600; + font-size: calc(var(--s) * 0.38); + color: #0E0D0B; +} +.av-current { box-shadow: 0 0 0 2px var(--bg, #fff), 0 0 0 4px var(--ac); } + +/* share sheet (NooSheet) */ +.sheet { + background: var(--sf); + border: 1px solid var(--ln); + border-radius: var(--radius-panel) var(--radius-panel) var(--radius-card) var(--radius-card); + padding: 10px 20px 22px; + box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 22px; + max-width: 440px; + margin-inline: auto; } - -.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; +.sheet-grab { display: block; width: 36px; height: 4px; border-radius: 4px; background: var(--sf3); margin: 0 auto -6px; } +.sheet-title { display: flex; align-items: baseline; gap: 10px; } +.sheet-title b { font-family: var(--font-display); font-size: 18px; font-weight: 600; } +.sheet-title i { font-style: normal; font-size: 13px; color: var(--fg3); } +.sh-sec { display: flex; flex-direction: column; gap: 4px; } +.sh-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; } +.sh-head h5 { font-size: 16px; } +.sh-cap { margin: 4px 0 0; font-size: 13px; color: var(--fg3); } +.field { + display: flex; align-items: center; gap: 10px; + height: 40px; padding: 0 14px; + background: var(--sf2); border-radius: var(--radius-pill); - font-size: 13px; - font-weight: 600; - transition: background var(--motion-base) var(--ease), color var(--motion-base) var(--ease); + color: var(--fg3); + margin-bottom: 4px; } -.demo-row { padding: 6px 0; } +.field input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font: inherit; font-size: 14px; color: var(--fg1); } +.prow { display: flex; align-items: center; gap: 12px; min-height: 52px; } +.owner { font-size: 13px; font-weight: 600; color: var(--fg3); } +.menu-wrap { position: relative; } +.pill { + display: inline-flex; align-items: center; gap: 4px; + height: 32px; padding: 0 10px 0 12px; + border: 0; border-radius: var(--radius-pill); + background: var(--sf2); + font-size: 13px; font-weight: 600; + cursor: pointer; + color: var(--fg1); +} +.pill .i { color: var(--fg2); } +.mini-menu { + position: absolute; right: 0; top: 38px; z-index: 5; + min-width: 170px; + background: var(--sf); + border: 1px solid var(--ln); + border-radius: 10px; + padding: 4px; + box-shadow: var(--shadow); + display: flex; flex-direction: column; +} +.mini-menu button { + display: flex; align-items: center; justify-content: space-between; gap: 12px; + border: 0; background: transparent; text-align: left; + padding: 9px 10px; border-radius: 8px; + font-size: 14px; cursor: pointer; +} +.mini-menu button:hover { background: var(--sf2); } +.mini-menu .danger { color: var(--dg); } -.swatch-row { display: flex; gap: 10px; } +/* ---------- customize lab ---------- */ + +.lab { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: clamp(28px, 5vw, 64px); + align-items: start; + margin-top: 44px; +} +.lab-phone { position: sticky; top: 96px; text-align: center; } +.lab-hint { font-size: 13px; color: var(--fg3); margin: 12px 0 0; } + +.lab-controls { display: flex; flex-direction: column; gap: 16px; } +.ctl { + background: var(--bg); + border-radius: var(--radius-card); + padding: 22px 24px; +} +.ctl h3 { font-size: 18px; margin-bottom: 6px; } +.ctl > p { margin: 0 0 16px; color: var(--fg2); font-size: 14px; } +.ctl-value { color: var(--act); font-weight: 600; } +.ctl-row { + display: flex; align-items: center; justify-content: space-between; gap: 16px; + margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--ln); +} +.ctl-row > span { display: flex; flex-direction: column; } +.ctl-row b { font-size: 15px; font-weight: 500; } +.ctl-row i { font-style: normal; font-size: 13px; color: var(--fg3); } +.ctl-row.is-disabled { opacity: 0.45; } +.ctl-row .toggle:disabled { cursor: not-allowed; } + +.swatch-row { display: flex; gap: 12px; flex-wrap: wrap; } .swatch-btn { - width: 32px; height: 32px; + width: 36px; height: 36px; border-radius: 50%; border: 2px solid transparent; background: var(--c); cursor: pointer; padding: 0; + color: #fff; + display: inline-flex; align-items: center; justify-content: center; + box-shadow: 0 0 0 2px var(--bg) inset; 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); } +.swatch-btn[data-accent="wallpaper"] { background: linear-gradient(135deg, #8DCDE2, #DA9AC7 50%, #E5D8BD); color: var(--fg1); } +.swatch-btn.is-active { border-color: var(--fg1); transform: scale(1.1); } +.swatch-btn:hover { transform: scale(1.1); } +/* NooSegmentedControl */ .segmented { display: inline-flex; padding: 3px; - background: var(--surface); + background: var(--sf2); border-radius: var(--radius-pill); gap: 2px; - align-self: flex-start; } +.section-alt .segmented { background: var(--sf2); } +.segmented-full { display: flex; } +.segmented-full .segmented-btn { flex: 1; } .segmented-btn { border: none; background: transparent; - padding: 8px 16px; + padding: 9px 16px; border-radius: var(--radius-pill); - font-family: var(--font-body); - font-size: 13px; + font-size: 14px; 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); } +.segmented-btn.is-active { background: var(--acs); color: var(--act); } +.segmented-sm .segmented-btn { padding: 6px 12px; font-size: 13px; } -.customize-list { - grid-column: 1 / -1; - background: var(--bg); +/* tab / action reorder list (NooTabOrderRow) */ +.tab-list { + list-style: none; margin: 0; padding: 0; + display: flex; flex-direction: column; gap: 1px; + background: var(--ln); border-radius: var(--radius-card); - padding: 28px 32px; + overflow: hidden; } -.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; +.tab-row { + display: flex; align-items: center; gap: 12px; + min-height: 52px; padding: 6px 16px 6px 8px; + background: var(--sf); + user-select: none; } -.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; +.tab-row.is-dragging { background: var(--acs); position: relative; z-index: 2; box-shadow: var(--shadow); } +.grip { display: inline-flex; padding: 8px; color: var(--fg3); cursor: grab; touch-action: none; } +.tab-ic { display: inline-flex; color: var(--fg2); } +.tab-label { flex: 1; font-size: 15px; font-weight: 500; } +.where { font-size: 12px; font-weight: 600; color: var(--fg3); padding: 3px 10px; border-radius: var(--radius-pill); background: var(--sf2); } +.where.is-bar { background: var(--acs); color: var(--act); } +.ctl .tab-row { background: var(--sf); } + +.duo { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-top: 16px; } +.duo .mock-card { background: var(--bg); box-shadow: none; border: 0; padding: 24px; } +.duo .tab-row { background: var(--sf); } + +@media (max-width: 1000px) { + .lab { grid-template-columns: 1fr; } + .lab-phone { position: static; } + .duo { grid-template-columns: 1fr; } } -@media (max-width: 760px) { - .customize-grid { grid-template-columns: 1fr; } - .feature-list { grid-template-columns: 1fr; } +/* selection action bar (NooSelectionBar) */ +.selbar-stage { position: relative; margin-bottom: 18px; } +.selbar { + height: 48px; + display: flex; align-items: center; gap: 8px; + padding: 0 6px; + background: var(--sf); + border-radius: var(--radius-card); } +.selbar-x { width: 36px; height: 36px; border-radius: 50%; background: var(--sf2); display: inline-flex; align-items: center; justify-content: center; } +.selbar b { font-family: var(--font-display); font-size: 17px; font-weight: 600; } +.selbar-acts { margin-left: auto; display: flex; align-items: center; } +.selbar-btn { border: 0; background: transparent; padding: 10px; color: var(--act); cursor: pointer; border-radius: 50%; display: inline-flex; } +.selbar-btn:hover { background: var(--acs); } +.sheet-mini { + margin-top: 8px; + background: var(--sf); + border-radius: var(--radius-panel) var(--radius-panel) var(--radius-card) var(--radius-card); + padding: 10px 16px 14px; + box-shadow: var(--shadow); + display: flex; flex-direction: column; gap: 1px; +} +.sheet-mini .sheet-grab { margin-bottom: 8px; } +.sheet-mini .srow { background: var(--sf2); } +.sheet-mini .srow:first-of-type { border-radius: var(--radius-card) var(--radius-card) 0 0; } +.sheet-mini .srow:last-child { border-radius: 0 0 var(--radius-card) var(--radius-card); } +.sheet-mini .srow:only-of-type { border-radius: var(--radius-card); } -/* ---------- privacy ---------- */ +/* swipe demo */ +.swipe-pick { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; flex-wrap: wrap; } +.swipe-pick > span { width: 84px; font-size: 14px; font-weight: 600; color: var(--fg2); } +.swipe-stage { position: relative; margin-top: 16px; } +.swipe-list { background: var(--sf); border-radius: var(--radius-card); overflow: hidden; } +.sw { position: relative; height: 64px; overflow: hidden; transition: height var(--motion-base) var(--ease), opacity var(--motion-base) var(--ease); touch-action: pan-y; user-select: none; cursor: grab; border-bottom: 1px solid var(--ln); } +.sw:last-child { border-bottom: 0; } +.sw.is-gone { height: 0; opacity: 0; } +.sw-fg { position: absolute; inset: 0; background: var(--sf); transform: translateX(var(--dx, 0px)); transition: transform var(--motion-base) var(--ease); } +.sw.is-dragging .sw-fg { transition: none; } +.sw-block { + position: absolute; top: 0; bottom: 0; + color: #fff; + display: none; + align-items: center; +} +.sw-right { left: 0; right: 0; justify-content: flex-start; } +.sw-left { left: 0; right: 0; justify-content: flex-end; } +.sw.show-right .sw-right, .sw.show-left .sw-left { display: flex; } +.sw-inner { width: 96px; display: flex; flex-direction: column; align-items: center; gap: 4px; transition: transform var(--motion-fast) var(--ease); } +.sw-inner b { font-size: 12px; font-weight: 600; } +.sw.is-through .sw-inner { transform: scale(1.12); } +.swipe-toast { + position: absolute; left: 12px; right: 12px; bottom: 12px; + background: var(--fg1); color: var(--bg); + padding: 10px 14px; border-radius: var(--radius-input); + font-size: 13px; font-weight: 500; + opacity: 0; transform: translateY(8px); + transition: opacity var(--motion-base) var(--ease), transform var(--motion-base) var(--ease); + pointer-events: none; +} +.swipe-toast.is-on { opacity: 1; transform: none; } + +/* ---------- tab grid ---------- */ + +.tab-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin-top: 44px; } +.tab-chip { + display: flex; flex-direction: column; gap: 14px; + background: var(--sf); + border-radius: var(--radius-card); + padding: 22px; +} +.tab-chip-ic { + display: inline-flex; align-items: center; justify-content: center; + width: 44px; height: 44px; border-radius: var(--radius-input); + background: var(--acs); color: var(--act); +} +.tab-chip h3 { font-size: 18px; margin-bottom: 4px; } +.tab-chip p { margin: 0; color: var(--fg2); font-size: 14px; } +@media (max-width: 960px) { .tab-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 480px) { .tab-grid { grid-template-columns: 1fr; } } + +/* ---------- privacy teaser ---------- */ .privacy-grid { display: grid; @@ -698,56 +779,219 @@ h1, h2, h3 { gap: clamp(32px, 6vw, 64px); align-items: center; } - -.privacy-points { - list-style: none; - margin: 0; - padding: 0; - display: grid; - gap: 14px; -} +.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); + display: flex; align-items: center; gap: 14px; + background: var(--bg); border-radius: var(--radius-input); padding: 16px 18px; font-weight: 500; } - -@media (max-width: 820px) { - .privacy-grid { grid-template-columns: 1fr; } +.pt-ic { + display: inline-flex; align-items: center; justify-content: center; + flex: none; width: 40px; height: 40px; + border-radius: var(--radius-input); + background: var(--acs); color: var(--act); } +@media (max-width: 820px) { .privacy-grid { grid-template-columns: 1fr; } } /* ---------- cta ---------- */ -.section-cta { text-align: center; background: var(--surface); } +.section-cta { text-align: center; } .cta-inner { display: flex; flex-direction: column; align-items: center; } +.section-cta h2 { max-width: none; } .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; } + +.soon-list { list-style: none; margin: 24px 0 0; padding: 0; display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; } +.soon-list li { + display: inline-flex; align-items: center; gap: 10px; + padding: 9px 16px; + border: 1px solid var(--ln); + border-radius: var(--radius-pill); + font-weight: 600; font-size: 14px; + color: var(--fg2); +} +.soon-list span { font-size: 12px; font-weight: 600; color: var(--act); background: var(--acs); padding: 2px 10px; border-radius: var(--radius-pill); } +.oss-note { display: flex; align-items: center; gap: 8px; margin: 28px 0 0; color: var(--fg2); font-size: 15px; justify-content: center; } /* ---------- footer ---------- */ -.site-footer { border-top: 1px solid var(--line); padding: 32px 0; } -.footer-row { - display: flex; - align-items: center; - gap: 24px; - flex-wrap: wrap; -} +.site-footer { border-top: 1px solid var(--ln); 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); } +/* ---------- phone mockup (Android layout) ---------- */ + +.phone-host { width: 356px; } +.np { + --frame: #1B1A17; + position: relative; + width: 356px; + height: 700px; + border: 8px solid var(--frame); + border-radius: 44px; + background: var(--bg); + color: var(--fg1); + overflow: hidden; + display: flex; + flex-direction: column; + font-family: var(--font-body); + text-align: left; + line-height: 1.3; + box-shadow: var(--shadow); + transition: background var(--motion-base) var(--ease); +} +.np button { cursor: pointer; border: 0; background: transparent; padding: 0; } + +.np-status { + height: 28px; flex: none; + display: flex; align-items: center; justify-content: space-between; + padding: 0 22px; + font-size: 12px; font-weight: 600; +} +.np-status-i { display: inline-flex; gap: 5px; align-items: center; } + +.np-top { + height: 64px; flex: none; + display: flex; align-items: center; gap: 8px; + padding: 0 12px 0 8px; +} +.np-ib { width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; color: var(--fg1); } +.np-top h4 { font-size: 22px; line-height: 1; padding-left: 8px; } +.np-ib + h4 { padding-left: 0; } +.np-top h4:has(+ .np-search) { max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.np-top h4:not(:has(+ .np-search)) { flex: 1; } +.np-search { + flex: 1; min-width: 0; + height: 40px; + display: flex; align-items: center; gap: 8px; + padding: 0 14px; + border-radius: var(--radius-pill); + background: var(--sf); + color: var(--fg3); + font-size: 14px; +} +.np-avbtn { margin-left: auto; display: inline-flex; border-radius: 50%; } +.np-search + .np-avbtn { margin-left: 0; } + +.np-body { flex: 1; overflow: hidden; padding-bottom: 8px; } +.np-list { display: flex; flex-direction: column; } +.fr { + display: flex; align-items: center; gap: 12px; + height: 64px; padding: 0 16px; + border-bottom: 1px solid var(--ln); +} +.sw .fr { height: 64px; border-bottom: 0; } +.ft { width: 40px; height: 40px; flex: none; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; } +.fm { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; } +.fm b { font-size: 15px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg1); } +.fm i { font-style: normal; font-size: 13px; color: var(--fg3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.fb { display: inline-flex; gap: 6px; flex: none; align-items: center; } +.b-fav { color: var(--wn); fill: var(--wn); } +.b-ok { color: var(--ok); } +.b-sync { color: var(--act); } +.b-muted { color: var(--fg3); } + +.np-photos { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; padding: 3px 3px 0; } +.np-photos span { aspect-ratio: 1; border-radius: 4px; } + +.np-fab { + position: absolute; right: 16px; + width: 56px; height: 56px; + border-radius: 16px; + background: var(--ac); color: #fff; + display: inline-flex; align-items: center; justify-content: center; + box-shadow: 0 8px 20px rgba(30, 0, 47, 0.25); + z-index: 3; +} + +/* bottom bar - NooBottomBar */ +.np-bar { position: absolute; left: 0; right: 0; bottom: 0; z-index: 4; } +.np-bar-row { display: flex; } +.nb { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; color: var(--fg2); } +.nb-pill { + width: 56px; height: 32px; border-radius: var(--radius-pill); + display: inline-flex; align-items: center; justify-content: center; + transition: background var(--motion-base) var(--ease); +} +.nb-label { margin-top: 4px; font-size: 12px; font-weight: 500; line-height: 1; opacity: 0; color: var(--fg1); transition: opacity var(--motion-base) var(--ease); } +.nb.is-sel { color: var(--act); } +.nb.is-sel .nb-pill { background: var(--acs); } +.nb.is-sel .nb-label { opacity: 1; font-weight: 600; } + +.np-bar.is-attached { background: var(--sf); border-top: 1px solid var(--ln); } +.np-bar.is-attached .np-bar-row { height: 80px; } +.np-bar.is-attached .nb { padding-top: 14px; } + +.np-bar.is-floating { display: flex; gap: 8px; padding: 0 16px 12px; } +.np-bar.is-floating .np-bar-row { + flex: 1; height: 64px; + background: var(--sf); + border: 1px solid var(--ln); + border-radius: 28px; + box-shadow: 0 24px 48px rgba(30, 0, 47, 0.14); + overflow: hidden; +} +.np-bar.is-floating .nb { padding-top: 8px; } +.np-bar.is-floating .nb-icon { justify-content: center; padding-top: 0; color: var(--fg2); } +.nb-sat { + width: 64px; height: 64px; flex: none; + border-radius: 50%; + background: var(--sf); + border: 1px solid var(--ln); + box-shadow: 0 24px 48px rgba(30, 0, 47, 0.14); + color: var(--fg1); + display: inline-flex; align-items: center; justify-content: center; +} + +/* drawer + avatar menu */ +.np-scrim { position: absolute; inset: 0; background: var(--scrim); z-index: 6; } +.np-drawer { + position: absolute; top: 0; bottom: 0; left: 0; width: 78%; + background: var(--sf); + border-radius: 0 28px 28px 0; + padding: 44px 12px 16px; + z-index: 7; + display: flex; flex-direction: column; gap: 2px; + animation: drawerIn var(--motion-base) var(--ease); +} +@keyframes drawerIn { from { transform: translateX(-24px); opacity: 0; } to { transform: none; opacity: 1; } } +.np-drawer-head { display: flex; align-items: center; gap: 8px; padding: 6px 12px 16px; font-family: var(--font-display); font-size: 22px; font-weight: 700; letter-spacing: -0.03em; } +.np-mark { width: 30px; height: 30px; background: var(--ac); -webkit-mask: url("noo-mark.png") center / 132% no-repeat; mask: url("noo-mark.png") center / 132% no-repeat; } +.np-dr { + display: flex; align-items: center; gap: 14px; + height: 48px; padding: 0 14px; + border-radius: var(--radius-pill); + font-size: 15px; font-weight: 500; color: var(--fg1); + text-align: left; width: 100%; +} +.np-dr:hover { background: var(--sf2); } +.np-sep { height: 1px; background: var(--ln); margin: 8px 8px; } +.np-pop { + position: absolute; top: 80px; right: 12px; width: 260px; + background: var(--sf); + border: 1px solid var(--ln); + border-radius: var(--radius-card); + padding: 8px; + z-index: 7; + box-shadow: 0 24px 48px rgba(30, 0, 47, 0.14); + animation: drawerIn var(--motion-base) var(--ease); +} +.np-acct { display: flex; align-items: center; gap: 12px; padding: 8px 10px; } +.np-acct span:not(.av) { flex: 1; min-width: 0; display: flex; flex-direction: column; } +.np-acct b { font-size: 14px; font-weight: 600; } +.np-acct i { font-style: normal; font-size: 12px; color: var(--fg3); } +.np-pop .av-current { box-shadow: none; } + /* ---------- motion ---------- */ @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .phone, .chip, .btn { transition: none !important; } + .chip, .btn, .np-drawer, .np-pop, .sw-fg, .toggle, .toggle i { transition: none !important; animation: none !important; } } .reveal { @@ -756,3 +1000,33 @@ h1, h2, h3 { transition: opacity 600ms var(--ease), transform 600ms var(--ease); } .reveal.is-visible { opacity: 1; transform: none; } + +/* ---------- long-form pages (privacy policy) ---------- */ + +.doc { padding: clamp(40px, 6vw, 72px) 0 clamp(56px, 8vw, 104px); } +.doc-wrap { max-width: 760px; margin: 0 auto; padding-inline: var(--gutter); } +.doc h1 { font-size: clamp(34px, 4.6vw, 52px); line-height: 1.05; margin-bottom: 12px; } +.doc .doc-meta { color: var(--fg3); font-size: 14px; margin: 0 0 32px; } +.doc .doc-lead { + font-size: 18px; color: var(--fg1); + background: var(--acs); + border-radius: var(--radius-card); + padding: 20px 24px; + margin: 0 0 40px; +} +.doc h2 { font-size: 24px; margin: 40px 0 12px; } +.doc h3 { font-size: 18px; margin: 24px 0 8px; } +.doc p, .doc li { color: var(--fg2); } +.doc p { margin: 0 0 14px; } +.doc ul, .doc ol { margin: 0 0 14px; padding-left: 22px; } +.doc li { margin-bottom: 6px; } +.doc a { color: var(--act); } +.doc strong { color: var(--fg1); font-weight: 600; } +.doc table { width: 100%; border-collapse: collapse; margin: 8px 0 20px; font-size: 14px; } +.doc th, .doc td { text-align: left; vertical-align: top; padding: 10px 12px; border-bottom: 1px solid var(--ln); } +.doc th { color: var(--fg1); font-weight: 600; background: var(--sf); } +.doc td { color: var(--fg2); } +.doc .toc { background: var(--sf); border-radius: var(--radius-card); padding: 18px 24px; margin: 0 0 8px; } +.doc .toc ol { margin: 0; columns: 2; column-gap: 32px; } +.doc .toc a { text-decoration: none; } +@media (max-width: 600px) { .doc .toc ol { columns: 1; } }