← back to Nineoh Guide
2026 graphics: Photoshop-baked grainy ombre hero (413KB jpg) + liquid-glass (backdrop-filter) header & frosted stat chips on web; mobile ombre LinearGradient header/bg + BlurView glass cards
3b392c3905c5ea2bce56f7d751e98ea684e22662 · 2026-07-25 13:35:50 -0700 · Steve
Files touched
M apps/mobile/App.tsxM apps/mobile/package.jsonM apps/web/app/globals.cssM apps/web/app/page.tsxM apps/web/next.config.mjsA apps/web/public/hero-ombre.jpgM pnpm-lock.yaml
Diff
commit 3b392c3905c5ea2bce56f7d751e98ea684e22662
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jul 25 13:35:50 2026 -0700
2026 graphics: Photoshop-baked grainy ombre hero (413KB jpg) + liquid-glass (backdrop-filter) header & frosted stat chips on web; mobile ombre LinearGradient header/bg + BlurView glass cards
---
apps/mobile/App.tsx | 34 +++++++++++++++++++++++-----------
apps/mobile/package.json | 2 ++
apps/web/app/globals.css | 37 ++++++++++++++++++++++++++++++++-----
apps/web/app/page.tsx | 20 ++++++++++----------
apps/web/next.config.mjs | 3 +++
apps/web/public/hero-ombre.jpg | Bin 0 -> 413290 bytes
pnpm-lock.yaml | 32 ++++++++++++++++++++++++++++++++
7 files changed, 102 insertions(+), 26 deletions(-)
diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 54b134d..a558723 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -10,6 +10,8 @@ import {
} from "react-native";
import { StatusBar } from "expo-status-bar";
import Constants from "expo-constants";
+import { LinearGradient } from "expo-linear-gradient";
+import { BlurView } from "expo-blur";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { APP, EpisodeSchema, type Episode } from "@nineoh/core";
@@ -128,15 +130,24 @@ export default function App() {
return (
<View style={styles.root}>
- <StatusBar style="auto" />
- <View style={styles.header}>
+ <LinearGradient
+ colors={["#f4e9f2", "#faf1ea", "#fdf3e0"]}
+ style={StyleSheet.absoluteFill}
+ />
+ <StatusBar style="light" />
+ <LinearGradient
+ colors={["#2b1e3a", "#7b3f6e", "#d9668a"]}
+ start={{ x: 0, y: 0 }}
+ end={{ x: 1, y: 1 }}
+ style={styles.header}
+ >
<Pressable onLongPress={titleFact} delayLongPress={450}>
<Text style={styles.title}>{APP.displayName}</Text>
</Pressable>
<Pressable onPress={tapBadge} hitSlop={8}>
<Text style={styles.badge}>UNOFFICIAL</Text>
</Pressable>
- </View>
+ </LinearGradient>
{egg ? (
<Pressable style={styles.egg} onPress={() => setEgg(false)}>
@@ -187,7 +198,7 @@ export default function App() {
shown.map((ep) => {
const isSaved = saved.has(ep.id);
return (
- <View key={ep.id} style={styles.card}>
+ <BlurView key={ep.id} intensity={28} tint="light" style={styles.card}>
<View style={styles.cardRow}>
<Text style={styles.cardTitle}>
S{ep.seasonNumber}E{ep.episodeNumber} · {ep.title}
@@ -219,7 +230,7 @@ export default function App() {
>
<Text style={styles.markWatched}>✓ watched to here</Text>
</Pressable>
- </View>
+ </BlurView>
);
})
)}
@@ -229,7 +240,7 @@ export default function App() {
}
const styles = StyleSheet.create({
- root: { flex: 1, backgroundColor: "#fafafa" },
+ root: { flex: 1, backgroundColor: "transparent" },
header: {
paddingTop: 64,
paddingHorizontal: 20,
@@ -240,8 +251,8 @@ const styles = StyleSheet.create({
alignItems: "baseline",
gap: 10,
},
- title: { fontSize: 20, fontWeight: "700" },
- badge: { fontSize: 11, fontWeight: "700", color: "#8a1c1c" },
+ title: { fontSize: 20, fontWeight: "700", color: "#fff8f0" },
+ badge: { fontSize: 11, fontWeight: "700", color: "#ffd27a" },
toolbar: {
paddingHorizontal: 20,
paddingVertical: 8,
@@ -262,12 +273,13 @@ const styles = StyleSheet.create({
disclaimer: { fontSize: 12, color: "#666", marginBottom: 16 },
empty: { fontSize: 14, color: "#8a1c1c", lineHeight: 20 },
card: {
- backgroundColor: "#fff",
+ backgroundColor: "rgba(255,255,255,0.5)",
borderWidth: 1,
- borderColor: "#e5e5e5",
- borderRadius: 8,
+ borderColor: "rgba(255,255,255,0.65)",
+ borderRadius: 14,
padding: 14,
marginBottom: 10,
+ overflow: "hidden",
},
cardRow: { flexDirection: "row", justifyContent: "space-between", gap: 8 },
cardTitle: { fontSize: 15, fontWeight: "600", flex: 1 },
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 49a669d..ebb76d8 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -13,7 +13,9 @@
"@nineoh/core": "workspace:*",
"@react-native-async-storage/async-storage": "1.23.1",
"expo": "~52.0.0",
+ "expo-blur": "~14.0.0",
"expo-constants": "~17.0.0",
+ "expo-linear-gradient": "~14.0.0",
"expo-status-bar": "~2.0.0",
"react": "18.3.1",
"react-native": "0.76.5"
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index 1d1f235..7774e1b 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -49,9 +49,11 @@ h2 { font-size: clamp(19px, 3vw, 23px); margin: 28px 0 10px; }
/* ── Layout chrome ─────────────────────────────────────────────────────── */
.site-header {
position: sticky; top: 0; z-index: 20;
- background: rgba(250, 246, 239, 0.82);
- backdrop-filter: saturate(1.4) blur(10px);
- border-bottom: 1px solid var(--line);
+ background: rgba(250, 246, 239, 0.62);
+ -webkit-backdrop-filter: saturate(1.8) blur(18px);
+ backdrop-filter: saturate(1.8) blur(18px);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.4);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5), 0 1px 12px rgba(23, 19, 15, 0.05);
}
.site-header .bar {
max-width: var(--maxw); margin: 0 auto;
@@ -84,10 +86,31 @@ h2 { font-size: clamp(19px, 3vw, 23px); margin: 28px 0 10px; }
}
.hero .sky {
position: absolute; inset: 0; z-index: -2;
- background: linear-gradient(180deg, var(--dusk-1) 0%, var(--dusk-2) 46%, var(--dusk-3) 70%, var(--sun-1) 90%, var(--sun-2) 100%);
- background-size: 100% 220%;
+ /* Photoshop-baked grainy ombre (dusk→blush→gold) over the animated CSS gradient fallback. */
+ background:
+ url("/hero-ombre.jpg") center/cover no-repeat,
+ linear-gradient(180deg, var(--dusk-1) 0%, var(--dusk-2) 46%, var(--dusk-3) 70%, var(--sun-1) 90%, var(--sun-2) 100%);
+ background-size: cover, 100% 220%;
animation: skyDrift 24s ease-in-out infinite alternate;
}
+
+/* ── Liquid glass (2026 glassmorphism) ─────────────────────────────────── */
+.glass {
+ background: rgba(255, 253, 249, 0.5);
+ -webkit-backdrop-filter: blur(18px) saturate(1.8);
+ backdrop-filter: blur(18px) saturate(1.8);
+ border: 1px solid rgba(255, 255, 255, 0.38);
+ box-shadow:
+ 0 8px 32px rgba(23, 19, 15, 0.10),
+ inset 0 1px 0 rgba(255, 255, 255, 0.55);
+}
+.glass-dark {
+ background: rgba(30, 20, 40, 0.34);
+ -webkit-backdrop-filter: blur(16px) saturate(1.6);
+ backdrop-filter: blur(16px) saturate(1.6);
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ color: #fff6ec;
+}
.hero .art { position: absolute; inset: 0; z-index: -1; width: 100%; height: 100%; }
.hero .eyebrow {
font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase;
@@ -103,6 +126,10 @@ h2 { font-size: clamp(19px, 3vw, 23px); margin: 28px 0 10px; }
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.hero .cta:hover { transform: translateY(-1px); text-decoration: none; box-shadow: 0 10px 26px rgba(0,0,0,0.28); }
+.hero-stats { display: flex; gap: 12px; margin-top: 26px; flex-wrap: wrap; }
+.hero-stat { border-radius: 14px; padding: 12px 20px; min-width: 92px; }
+.hero-stat-num { font-family: var(--font-display, serif); font-size: 28px; font-weight: 700; color: #fff3d6; line-height: 1; }
+.hero-stat-lab { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(255,246,236,0.82); margin-top: 4px; }
/* ── Cards / stats ─────────────────────────────────────────────────────── */
.stat-row { display: flex; flex-wrap: wrap; gap: 14px; margin: 22px 0 6px; }
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
index e74a843..5712228 100644
--- a/apps/web/app/page.tsx
+++ b/apps/web/app/page.tsx
@@ -67,13 +67,13 @@ export default async function Home() {
<a className="cta rise-3" href="/episodes">
Browse the episode guide →
</a>
- </section>
- <div className="stat-row">
- <Stat num={episodes} lab="Episodes indexed" />
- <Stat num={recapped} lab="Original recaps" />
- <Stat num={cast} lab="Cast profiles" />
- </div>
+ <div className="hero-stats rise-3">
+ <GlassStat num={episodes} lab="Episodes" />
+ <GlassStat num={recapped} lab="Recaps" />
+ <GlassStat num={cast} lab="Cast" />
+ </div>
+ </section>
<h2>Start exploring</h2>
<div style={{ display: "grid", gap: 12, gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))" }}>
@@ -138,11 +138,11 @@ export default async function Home() {
);
}
-function Stat({ num, lab }: { num: number; lab: string }) {
+function GlassStat({ num, lab }: { num: number; lab: string }) {
return (
- <div className="stat">
- <div className="num">{num}</div>
- <div className="lab">{lab}</div>
+ <div className="glass-dark hero-stat">
+ <div className="hero-stat-num">{num}</div>
+ <div className="hero-stat-lab">{lab}</div>
</div>
);
}
diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs
index efb324c..93d781c 100644
--- a/apps/web/next.config.mjs
+++ b/apps/web/next.config.mjs
@@ -1,6 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
+ // Bundle everything into a self-contained server for pm2 deploy on Kamatera.
+ output: "standalone",
+ outputFileTracingRoot: new URL("../../", import.meta.url).pathname,
// packages/core is TS source consumed directly — let Next transpile it.
transpilePackages: ["@nineoh/core"],
// pg is a server-only dependency; keep it out of the client bundle.
diff --git a/apps/web/public/hero-ombre.jpg b/apps/web/public/hero-ombre.jpg
new file mode 100644
index 0000000..caea1ae
Binary files /dev/null and b/apps/web/public/hero-ombre.jpg differ
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8276148..930db21 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -27,9 +27,15 @@ importers:
expo:
specifier: ~52.0.0
version: 52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)
+ expo-blur:
+ specifier: ~14.0.0
+ version: 14.0.3(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
expo-constants:
specifier: ~17.0.0
version: 17.0.8(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(supports-color@8.1.1)
+ expo-linear-gradient:
+ specifier: ~14.0.0
+ version: 14.0.2(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
expo-status-bar:
specifier: ~2.0.0
version: 2.0.1(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)
@@ -1998,6 +2004,13 @@ packages:
react: '*'
react-native: '*'
+ expo-blur@14.0.3:
+ resolution: {integrity: sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
expo-constants@17.0.8:
resolution: {integrity: sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg==}
peerDependencies:
@@ -2022,6 +2035,13 @@ packages:
expo: '*'
react: '*'
+ expo-linear-gradient@14.0.2:
+ resolution: {integrity: sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+ react-native: '*'
+
expo-modules-autolinking@2.0.8:
resolution: {integrity: sha512-DezgnEYFQYic8hKGhkbztBA3QUmSftjaNDIKNAtS2iGJmzCcNIkatjN2slFDSWjSTNo8gOvPQyMKfyHWFvLpOQ==}
hasBin: true
@@ -6210,6 +6230,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ expo-blur@14.0.3(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
+ dependencies:
+ expo: 52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)
+ react: 18.3.1
+ react-native: 0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
+
expo-constants@17.0.8(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(supports-color@8.1.1):
dependencies:
'@expo/config': 10.0.11(supports-color@8.1.1)
@@ -6236,6 +6262,12 @@ snapshots:
expo: 52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)
react: 18.3.1
+ expo-linear-gradient@14.0.2(expo@52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1):
+ dependencies:
+ expo: 52.0.49(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1))(react@18.3.1)(supports-color@8.1.1)
+ react: 18.3.1
+ react-native: 0.76.5(@babel/core@7.29.7(supports-color@8.1.1))(@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@18.3.31)(react@18.3.1)(supports-color@8.1.1)
+
expo-modules-autolinking@2.0.8:
dependencies:
'@expo/spawn-async': 1.8.0
← 7a343d7 episode detail: prev/next adjacent-episode nav (row-comparis
·
back to Nineoh Guide
·
gitignore CTA test artifacts (cross-browser test passed: liq 396861c →