[object Object]

← back to CelebritySignatures

iOS app: white gallery theme + all images render + tap-signature works/books (Steve email 8/5)

91f1e57018792cb06960b17c24275a3e0785bc94 · 2026-08-05 17:33:50 -0700 · Steve Abrams

- Flip the charcoal liquid-glass palette to a white gallery wall (glass.tsx GLASS
  palette, scene gradient, light BlurView tints, dark StatusBar, per-screen
  surface fills) — 'make all white not black'
- sigImg(): route Commons SVG signatures (62 incl. Jefferson) through
  Special:FilePath?width= for a rasterized PNG; solid white backings so dark-ink
  transparent signatures are visible — 'make sure all images serve'
- SignatureDetail: new Works section — open-museum artworks for Artists
  (Met→AIC→Cleveland→Wikidata) and Open Library books for everyone else, tiles
  link out — 'on click of signature bring up artwork or book and details'
- Games inherit the fixes (stage images + light chrome); tsc + expo export clean

Files touched

Diff

commit 91f1e57018792cb06960b17c24275a3e0785bc94
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 5 17:33:50 2026 -0700

    iOS app: white gallery theme + all images render + tap-signature works/books (Steve email 8/5)
    
    - Flip the charcoal liquid-glass palette to a white gallery wall (glass.tsx GLASS
      palette, scene gradient, light BlurView tints, dark StatusBar, per-screen
      surface fills) — 'make all white not black'
    - sigImg(): route Commons SVG signatures (62 incl. Jefferson) through
      Special:FilePath?width= for a rasterized PNG; solid white backings so dark-ink
      transparent signatures are visible — 'make sure all images serve'
    - SignatureDetail: new Works section — open-museum artworks for Artists
      (Met→AIC→Cleveland→Wikidata) and Open Library books for everyone else, tiles
      link out — 'on click of signature bring up artwork or book and details'
    - Games inherit the fixes (stage images + light chrome); tsc + expo export clean
---
 apps/mobile/App.tsx                     |   2 +-
 apps/mobile/api.ts                      |  17 +++
 apps/mobile/screens/AccountScreen.tsx   |   6 +-
 apps/mobile/screens/GalleryScreen.tsx   |  13 +-
 apps/mobile/screens/GameScreen.tsx      |  22 ++--
 apps/mobile/screens/MuralsScreen.tsx    |   4 +-
 apps/mobile/screens/SignatureDetail.tsx | 204 +++++++++++++++++++++++++++++++-
 apps/mobile/ui/glass.tsx                |  41 ++++---
 8 files changed, 262 insertions(+), 47 deletions(-)

diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 446d692..4597fbb 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -73,7 +73,7 @@ export default function App() {
   return (
     <GlassScene>
       <SafeAreaView style={styles.safe}>
-        <StatusBar style="light" />
+        <StatusBar style="dark" />
 
         <View style={styles.body}>
           {tab === 'gallery' && <GalleryScreen onOpen={openSignature} />}
diff --git a/apps/mobile/api.ts b/apps/mobile/api.ts
index f367e42..b38d85f 100644
--- a/apps/mobile/api.ts
+++ b/apps/mobile/api.ts
@@ -44,6 +44,23 @@ export function qidOf(sig: Signature): string | null {
   return m ? m[0] : null;
 }
 
+/**
+ * RN's <Image> cannot decode SVG (62 of the 1,554 signatures, incl. Jefferson),
+ * so route Commons SVG originals through Special:FilePath?width=N, which 302s
+ * to a rasterized PNG thumb (the same mechanism the evolution data already
+ * uses; hand-building …/thumb/… paths 400s on some shards). Raster originals
+ * pass through untouched — they decode natively and the thumb service errors
+ * when asked to upscale a bitmap past its native width.
+ */
+export function sigImg(url?: string, width = 640): string | undefined {
+  if (!url) return undefined;
+  const m = url.match(
+    /^https?:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/[0-9a-f]\/[0-9a-f]{2}\/([^/?#]+\.svg)$/i,
+  );
+  if (!m) return url;
+  return `https://commons.wikimedia.org/wiki/Special:FilePath/${m[1]}?width=${width}`;
+}
+
 async function getJSON<T>(path: string): Promise<T> {
   const r = await fetch(`${WEB_BASE}${path}`);
   if (!r.ok) throw new Error(`${path} → ${r.status}`);
diff --git a/apps/mobile/screens/AccountScreen.tsx b/apps/mobile/screens/AccountScreen.tsx
index 927c124..ba89f62 100644
--- a/apps/mobile/screens/AccountScreen.tsx
+++ b/apps/mobile/screens/AccountScreen.tsx
@@ -455,7 +455,7 @@ const styles = StyleSheet.create({
     overflow: 'hidden',
     borderWidth: StyleSheet.hairlineWidth,
     borderColor: GLASS.edge,
-    backgroundColor: 'rgba(255,255,255,0.04)',
+    backgroundColor: '#ffffff',
     marginBottom: 24,
   },
   toggleSegment: {
@@ -485,7 +485,7 @@ const styles = StyleSheet.create({
   fieldWrap: {
     borderRadius: 12,
     overflow: 'hidden',
-    backgroundColor: 'rgba(255,255,255,0.07)',
+    backgroundColor: '#ffffff',
     borderWidth: StyleSheet.hairlineWidth,
     borderColor: GLASS.edgeFaint,
   },
@@ -544,7 +544,7 @@ const styles = StyleSheet.create({
   deleteSection: {
     paddingTop: 20,
     borderTopWidth: StyleSheet.hairlineWidth,
-    borderTopColor: 'rgba(255,255,255,0.08)',
+    borderTopColor: 'rgba(20,18,16,0.1)',
   },
   deleteCaption: {
     fontSize: 12,
diff --git a/apps/mobile/screens/GalleryScreen.tsx b/apps/mobile/screens/GalleryScreen.tsx
index 87eda50..f46c0d0 100644
--- a/apps/mobile/screens/GalleryScreen.tsx
+++ b/apps/mobile/screens/GalleryScreen.tsx
@@ -16,7 +16,7 @@ import {
 } from 'react-native';
 import * as Haptics from 'expo-haptics';
 
-import { api, qidOf, Signature } from '../api';
+import { api, qidOf, sigImg, Signature } from '../api';
 import { GLASS, GlassPanel, GlassPill, GlassButton } from '../ui/glass';
 
 // ---------------------------------------------------------------------------
@@ -86,11 +86,12 @@ function SigCard({
       style={styles.cardPressable}
     >
       <GlassPanel style={styles.card}>
-        {/* parchment image backing so dark-ink signatures are legible */}
+        {/* solid white image backing — signatures are dark ink on transparent,
+            so anything but white renders them invisible */}
         <View style={styles.imgBacking}>
           {sig.signature_image_url ? (
             <Image
-              source={{ uri: sig.signature_image_url }}
+              source={{ uri: sigImg(sig.signature_image_url) }}
               style={styles.img}
               resizeMode="contain"
             />
@@ -293,7 +294,7 @@ const styles = StyleSheet.create({
     marginBottom: 8,
     borderRadius: 12,
     overflow: 'hidden',
-    backgroundColor: 'rgba(255,255,255,0.07)',
+    backgroundColor: '#ffffff',
     borderWidth: StyleSheet.hairlineWidth,
     borderColor: GLASS.edgeFaint,
   },
@@ -338,7 +339,7 @@ const styles = StyleSheet.create({
     // GlassPanel handles borderRadius + overflow
   },
   imgBacking: {
-    backgroundColor: 'rgba(232,217,184,0.06)',
+    backgroundColor: '#ffffff',
     borderRadius: 10,
     height: 110,
     alignItems: 'center',
@@ -353,7 +354,7 @@ const styles = StyleSheet.create({
   imgPlaceholder: {
     width: '100%',
     height: '100%',
-    backgroundColor: 'rgba(255,255,255,0.03)',
+    backgroundColor: 'rgba(0,0,0,0.03)',
   },
   cardName: {
     fontSize: 14,
diff --git a/apps/mobile/screens/GameScreen.tsx b/apps/mobile/screens/GameScreen.tsx
index fe74c0b..579289b 100644
--- a/apps/mobile/screens/GameScreen.tsx
+++ b/apps/mobile/screens/GameScreen.tsx
@@ -36,7 +36,7 @@ import {
 } from 'react-native';
 import * as Haptics from 'expo-haptics';
 
-import { api, Signature, LeaderRow, Evolution, qidOf } from '../api';
+import { api, Signature, LeaderRow, Evolution, qidOf, sigImg } from '../api';
 import { GLASS, GlassPanel, GlassButton, GlassPill } from '../ui/glass';
 
 // ---------------------------------------------------------------------------
@@ -667,7 +667,7 @@ function Stage({ round }: { round: Round | undefined }) {
     return (
       <View style={styles.sigWrap}>
         <GlassPanel style={styles.sigPanel} intensity={18} tint="light">
-          <Image source={{ uri: round.answer.signature_image_url }} style={styles.sigImage} resizeMode="contain" accessibilityLabel="A handwritten signature" />
+          <Image source={{ uri: sigImg(round.answer.signature_image_url) }} style={styles.sigImage} resizeMode="contain" accessibilityLabel="A handwritten signature" />
         </GlassPanel>
       </View>
     );
@@ -688,7 +688,7 @@ function Stage({ round }: { round: Round | undefined }) {
         {(['leftUrl', 'rightUrl'] as const).map((k, idx) => (
           <View key={k} style={styles.duoCell}>
             <GlassPanel style={styles.sigPanel} intensity={18} tint="light">
-              <Image source={{ uri: round[k] }} style={styles.duoImg} resizeMode="contain" accessibilityLabel={`Signature ${idx === 0 ? 'A' : 'B'}`} />
+              <Image source={{ uri: sigImg(round[k]) }} style={styles.duoImg} resizeMode="contain" accessibilityLabel={`Signature ${idx === 0 ? 'A' : 'B'}`} />
             </GlassPanel>
             <Text style={styles.duoLab}>{idx === 0 ? 'A' : 'B'}</Text>
           </View>
@@ -952,7 +952,7 @@ const styles = StyleSheet.create({
   gameGrid: { gap: 10, marginTop: 4 },
   gcard: {
     borderRadius: 16, borderWidth: StyleSheet.hairlineWidth, borderColor: GLASS.edgeFaint,
-    backgroundColor: 'rgba(255,255,255,0.04)', padding: 14,
+    backgroundColor: '#ffffff', padding: 14,
   },
   gcardOn: { borderColor: GLASS.accent, backgroundColor: 'rgba(201,168,106,0.14)' },
   gcardTitle: { color: GLASS.accentSoft, fontSize: 17, fontWeight: '800', letterSpacing: 0.2 },
@@ -964,7 +964,7 @@ const styles = StyleSheet.create({
   chip: {
     flexDirection: 'row', alignItems: 'center',
     borderRadius: 999, borderWidth: StyleSheet.hairlineWidth, borderColor: GLASS.edgeFaint,
-    paddingHorizontal: 14, paddingVertical: 9, backgroundColor: 'rgba(255,255,255,0.04)',
+    paddingHorizontal: 14, paddingVertical: 9, backgroundColor: '#ffffff',
   },
   chipActive: { borderColor: GLASS.accent, backgroundColor: 'rgba(201,168,106,0.2)' },
   chipText: { color: GLASS.accentSoft, fontSize: 14, fontWeight: '600' },
@@ -978,7 +978,7 @@ const styles = StyleSheet.create({
   prompt: { color: GLASS.accentSoft, fontSize: 18, fontWeight: '700', textAlign: 'center', letterSpacing: 0.2, lineHeight: 24 },
   tag: { color: GLASS.accent, fontSize: 13, fontWeight: '800', letterSpacing: 0.8, textAlign: 'center', textTransform: 'uppercase' },
 
-  timerTrack: { height: 6, borderRadius: 3, backgroundColor: 'rgba(255,255,255,0.08)', overflow: 'hidden' },
+  timerTrack: { height: 6, borderRadius: 3, backgroundColor: 'rgba(0,0,0,0.08)', overflow: 'hidden' },
   timerFill: { height: 6, backgroundColor: GLASS.accent },
 
   sigWrap: {
@@ -986,13 +986,13 @@ const styles = StyleSheet.create({
     shadowOpacity: 0.22, shadowRadius: 24, elevation: 10,
   },
   sigPanel: {},
-  sigImage: { width: '100%', height: 190 },
+  sigImage: { width: '100%', height: 190, backgroundColor: '#ffffff', borderRadius: 8 },
   artImage: { width: '100%', height: 260, borderRadius: 8 },
 
   // early duo
   duo: { flexDirection: 'row', gap: 12 },
   duoCell: { flex: 1, alignItems: 'center', gap: 6 },
-  duoImg: { width: '100%', height: 150 },
+  duoImg: { width: '100%', height: 150, backgroundColor: '#ffffff', borderRadius: 8 },
   duoLab: { color: GLASS.accent, fontSize: 16, fontWeight: '800' },
 
   // choices
@@ -1006,10 +1006,10 @@ const styles = StyleSheet.create({
   matchGrid: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', gap: 10 },
   matchCell: {
     width: '48%', aspectRatio: 1, borderRadius: 14, overflow: 'hidden',
-    borderWidth: 2, backgroundColor: 'rgba(255,255,255,0.04)',
+    borderWidth: 2, backgroundColor: '#ffffff',
   },
   matchImg: { width: '100%', height: '100%' },
-  matchLabelWrap: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(13,11,10,0.82)', paddingVertical: 4, paddingHorizontal: 6 },
+  matchLabelWrap: { position: 'absolute', bottom: 0, left: 0, right: 0, backgroundColor: 'rgba(255,255,254,0.92)', paddingVertical: 4, paddingHorizontal: 6 },
   matchLabel: { color: GLASS.accentSoft, fontSize: 11, fontWeight: '600', textAlign: 'center' },
 
   // reveal
@@ -1027,7 +1027,7 @@ const styles = StyleSheet.create({
   rankText: { color: GLASS.accent, fontSize: 18, fontWeight: '700', textAlign: 'center' },
   nameInput: {
     borderRadius: 14, borderWidth: StyleSheet.hairlineWidth, borderColor: GLASS.edge,
-    backgroundColor: 'rgba(255,255,255,0.06)', color: GLASS.accentSoft, fontSize: 16,
+    backgroundColor: '#ffffff', color: GLASS.accentSoft, fontSize: 16,
     paddingHorizontal: 16, paddingVertical: 13,
   },
   submitBtn: { marginTop: 2 },
diff --git a/apps/mobile/screens/MuralsScreen.tsx b/apps/mobile/screens/MuralsScreen.tsx
index 1cd418d..2e657b4 100644
--- a/apps/mobile/screens/MuralsScreen.tsx
+++ b/apps/mobile/screens/MuralsScreen.tsx
@@ -475,10 +475,10 @@ const styles = StyleSheet.create({
     marginBottom: 4,
   },
   input: {
-    backgroundColor: 'rgba(255,255,255,0.06)',
+    backgroundColor: '#ffffff',
     borderRadius: 10,
     borderWidth: StyleSheet.hairlineWidth,
-    borderColor: 'rgba(255,255,255,0.14)',
+    borderColor: 'rgba(20,18,16,0.14)',
     paddingHorizontal: 13,
     paddingVertical: Platform.OS === 'ios' ? 12 : 9,
     color: GLASS.accentSoft,
diff --git a/apps/mobile/screens/SignatureDetail.tsx b/apps/mobile/screens/SignatureDetail.tsx
index 5ee9b7f..bf3fc4c 100644
--- a/apps/mobile/screens/SignatureDetail.tsx
+++ b/apps/mobile/screens/SignatureDetail.tsx
@@ -2,9 +2,11 @@
 // The App wraps this in a Modal with its own GlassBar top chrome;
 // this component is just the scrollable body content.
 // Usage: <SignatureDetail sig={sig} evolution={ev} onOrderMural={(sig) => …} />
-import React, { useCallback } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 import {
   Image,
+  Linking,
+  Pressable,
   ScrollView,
   StyleSheet,
   Text,
@@ -12,9 +14,125 @@ import {
 } from 'react-native';
 import * as Haptics from 'expo-haptics';
 
-import { qidOf, Signature, Evolution } from '../api';
+import { qidOf, sigImg, Signature, Evolution } from '../api';
 import { GLASS, GlassPanel, GlassButton, GlassPill } from '../ui/glass';
 
+// ---------------------------------------------------------------------------
+// Works loader — tapping a signature opens this detail, which surfaces the
+// person's ARTWORK (Artists — free open-museum APIs, tried in order until we
+// have 6: The Met → Art Institute of Chicago → Cleveland → Wikidata/Commons)
+// or their BOOKS (everyone else — Open Library, keyless). Mirrors the web
+// popup's loader in public/index.html.
+// ---------------------------------------------------------------------------
+
+type Work = { img: string; title: string; date: string; url: string; src: string };
+const WORKS_CACHE: Record<string, Work[]> = {};
+const MAX_WORKS = 6;
+
+async function loadArtworks(qid: string | null, name: string): Promise<Work[]> {
+  const works: Work[] = [];
+  const last = name.toLowerCase().split(' ').pop() || '';
+  // 1) The Met
+  try {
+    const MET = 'https://collectionapi.metmuseum.org/public/collection/v1';
+    const s = await (await fetch(`${MET}/search?hasImages=true&artistOrCulture=true&q=${encodeURIComponent(name)}`)).json();
+    for (const id of (s.objectIDs || []).slice(0, 14)) {
+      if (works.length >= MAX_WORKS) break;
+      try {
+        const o = await (await fetch(`${MET}/objects/${id}`)).json();
+        if (!o.primaryImageSmall) continue;
+        if (!(o.artistDisplayName || '').toLowerCase().includes(last)) continue;
+        works.push({
+          img: o.primaryImageSmall, title: o.title || 'Untitled', date: o.objectDate || '',
+          url: o.objectURL || `https://www.metmuseum.org/art/collection/search/${id}`, src: 'The Met',
+        });
+      } catch { /* skip object */ }
+    }
+  } catch { /* Met unavailable */ }
+  // 2) Art Institute of Chicago
+  if (works.length < MAX_WORKS) try {
+    const j = await (await fetch(
+      `https://api.artic.edu/api/v1/artworks/search?q=${encodeURIComponent(name)}` +
+      '&query[term][is_public_domain]=true&fields=id,title,image_id,artist_title,date_display&limit=12',
+    )).json();
+    for (const a of (j.data || [])) {
+      if (works.length >= MAX_WORKS) break;
+      if (!a.image_id || !(a.artist_title || '').toLowerCase().includes(last)) continue;
+      works.push({
+        img: `https://www.artic.edu/iiif/2/${a.image_id}/full/400,/0/default.jpg`,
+        title: a.title || 'Untitled', date: a.date_display || '',
+        url: `https://www.artic.edu/artworks/${a.id}`, src: 'Art Institute of Chicago',
+      });
+    }
+  } catch { /* AIC unavailable */ }
+  // 3) Cleveland Museum of Art
+  if (works.length < MAX_WORKS) try {
+    const j = await (await fetch(
+      `https://openaccess-api.clevelandart.org/api/artworks/?artists=${encodeURIComponent(name)}&has_image=1&limit=10`,
+    )).json();
+    for (const a of (j.data || [])) {
+      if (works.length >= MAX_WORKS) break;
+      const img = a.images?.web?.url;
+      if (!img) continue;
+      if (!(a.creators || []).some((c: any) => (c.description || '').toLowerCase().includes(last))) continue;
+      works.push({
+        img, title: a.title || 'Untitled', date: a.creation_date || '',
+        url: a.url || 'https://www.clevelandart.org/art/collection/search', src: 'Cleveland Museum of Art',
+      });
+    }
+  } catch { /* Cleveland unavailable */ }
+  // 4) Wikidata works-by-QID (P170 creator + P18 image) — QID-exact fallback
+  if (qid && works.length < MAX_WORKS) try {
+    const q = `SELECT ?w ?wLabel ?img ?date WHERE { ?w wdt:P170 wd:${qid} ; wdt:P18 ?img . OPTIONAL { ?w wdt:P571 ?date } SERVICE wikibase:label { bd:serviceParam wikibase:language "en". } } LIMIT ${MAX_WORKS * 2}`;
+    const j = await (await fetch(
+      `https://query.wikidata.org/sparql?format=json&query=${encodeURIComponent(q)}`,
+      { headers: { Accept: 'application/sparql-results+json' } },
+    )).json();
+    for (const b of (j.results?.bindings || [])) {
+      if (works.length >= MAX_WORKS) break;
+      const m = (b.img?.value || '').match(/Special:FilePath\/(.+)$/);
+      if (!m) continue;
+      works.push({
+        img: `https://commons.wikimedia.org/wiki/Special:FilePath/${m[1]}?width=400`,
+        title: b.wLabel?.value || 'Untitled', date: (b.date?.value || '').slice(0, 4),
+        url: b.w?.value || '', src: 'Wikimedia Commons',
+      });
+    }
+  } catch { /* WDQS unavailable */ }
+  return works;
+}
+
+async function loadBooks(name: string): Promise<Work[]> {
+  const works: Work[] = [];
+  try {
+    const j = await (await fetch(
+      `https://openlibrary.org/search.json?author=${encodeURIComponent(name)}&sort=editions&limit=10&fields=title,cover_i,first_publish_year,key`,
+    )).json();
+    for (const d of (j.docs || [])) {
+      if (works.length >= MAX_WORKS) break;
+      if (!d.cover_i) continue;
+      works.push({
+        img: `https://covers.openlibrary.org/b/id/${d.cover_i}-M.jpg`,
+        title: d.title || 'Untitled',
+        date: d.first_publish_year ? String(d.first_publish_year) : '',
+        url: d.key ? `https://openlibrary.org${d.key}` : 'https://openlibrary.org',
+        src: 'Open Library',
+      });
+    }
+  } catch { /* Open Library unavailable */ }
+  return works;
+}
+
+async function loadWorks(sig: Signature): Promise<Work[]> {
+  const key = qidOf(sig) || sig.full_name;
+  if (WORKS_CACHE[key]) return WORKS_CACHE[key];
+  const out = sig.category === 'Artists'
+    ? await loadArtworks(qidOf(sig), sig.full_name)
+    : await loadBooks(sig.full_name);
+  WORKS_CACHE[key] = out;
+  return out;
+}
+
 // ---------------------------------------------------------------------------
 // Sub-components
 // ---------------------------------------------------------------------------
@@ -82,6 +200,15 @@ export default function SignatureDetail({
   const qid = qidOf(sig);
   const evEntry = qid && evolution ? evolution[qid] ?? null : null;
 
+  // works (artwork or books) — loaded per signature; null = still loading
+  const [works, setWorks] = useState<Work[] | null>(null);
+  useEffect(() => {
+    let cancelled = false;
+    setWorks(null);
+    loadWorks(sig).then((w) => { if (!cancelled) setWorks(w); }).catch(() => { if (!cancelled) setWorks([]); });
+    return () => { cancelled = true; };
+  }, [sig]);
+
   const handleOrder = useCallback(() => {
     Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
     onOrderMural(sig);
@@ -98,7 +225,7 @@ export default function SignatureDetail({
         <View style={styles.heroBacking}>
           {sig.signature_image_url ? (
             <Image
-              source={{ uri: sig.signature_image_url }}
+              source={{ uri: sigImg(sig.signature_image_url) }}
               style={styles.heroImg}
               resizeMode="contain"
             />
@@ -139,6 +266,38 @@ export default function SignatureDetail({
         <EvolutionReel sigs={evEntry.sigs} />
       )}
 
+      {/* Works — the person's artwork (Artists) or books (everyone else) */}
+      <View style={styles.section}>
+        <Text style={styles.sectionTitle}>
+          {sig.category === 'Artists' ? 'Works — open museum collections' : 'Books'}
+        </Text>
+        {works === null ? (
+          <Text style={styles.creditText}>loading…</Text>
+        ) : works.length === 0 ? (
+          <Text style={styles.creditText}>
+            {sig.category === 'Artists'
+              ? 'No openly-licensed works found in the open museum APIs.'
+              : 'No books found on Open Library.'}
+          </Text>
+        ) : (
+          <View style={styles.worksGrid}>
+            {works.map((w) => (
+              <Pressable
+                key={w.url + w.title}
+                style={styles.workTile}
+                onPress={() => { Haptics.selectionAsync(); Linking.openURL(w.url).catch(() => {}); }}
+              >
+                <Image source={{ uri: w.img }} style={styles.workImg} resizeMode="cover" />
+                <Text style={styles.workTitle} numberOfLines={2}>
+                  {w.title}{w.date ? ` · ${w.date}` : ''}
+                </Text>
+                <Text style={styles.workSrc} numberOfLines={1}>{w.src}</Text>
+              </Pressable>
+            ))}
+          </View>
+        )}
+      </View>
+
       {/* Image credit */}
       {(sig.image_author || sig.image_license) && (
         <View style={styles.section}>
@@ -180,7 +339,7 @@ const styles = StyleSheet.create({
     // GlassPanel adds borderRadius + panelBody padding
   },
   heroBacking: {
-    backgroundColor: 'rgba(232,217,184,0.07)',
+    backgroundColor: '#ffffff',
     borderRadius: 12,
     height: 200,
     alignItems: 'center',
@@ -263,7 +422,7 @@ const styles = StyleSheet.create({
   reelImgBacking: {
     width: 120,
     height: 80,
-    backgroundColor: 'rgba(232,217,184,0.07)',
+    backgroundColor: '#ffffff',
     borderRadius: 10,
     overflow: 'hidden',
     borderWidth: StyleSheet.hairlineWidth,
@@ -287,6 +446,41 @@ const styles = StyleSheet.create({
     letterSpacing: 0.15,
   },
 
+  // Works grid (artworks / book covers)
+  worksGrid: {
+    flexDirection: 'row',
+    flexWrap: 'wrap',
+    gap: 10,
+  },
+  workTile: {
+    width: '48%',
+    borderRadius: 12,
+    overflow: 'hidden',
+    backgroundColor: '#ffffff',
+    borderWidth: StyleSheet.hairlineWidth,
+    borderColor: GLASS.edgeFaint,
+    paddingBottom: 8,
+  },
+  workImg: {
+    width: '100%',
+    height: 150,
+    backgroundColor: 'rgba(0,0,0,0.04)',
+  },
+  workTitle: {
+    fontSize: 12,
+    fontWeight: '600',
+    color: GLASS.accentSoft,
+    lineHeight: 16,
+    paddingHorizontal: 8,
+    paddingTop: 6,
+  },
+  workSrc: {
+    fontSize: 10,
+    color: GLASS.textDim,
+    paddingHorizontal: 8,
+    paddingTop: 2,
+  },
+
   // CTA
   ctaWrap: {
     marginTop: 4,
diff --git a/apps/mobile/ui/glass.tsx b/apps/mobile/ui/glass.tsx
index a40d992..a2f1cd0 100644
--- a/apps/mobile/ui/glass.tsx
+++ b/apps/mobile/ui/glass.tsx
@@ -6,21 +6,24 @@ import { BlurView } from 'expo-blur';
 import { LinearGradient } from 'expo-linear-gradient';
 
 // Liquid Glass primitives (reused from the Charge & Explore app, recolored to a
-// museum charcoal-and-gold palette for the signatures gallery). Every surface is
-// a real BlurView refracting the gradient scene behind it — not a faked rgba fill
-// — with a specular top edge so panels read as curved glass. Android's BlurView
-// needs the experimental method or it silently renders a gray slab.
+// white gallery-wall palette for the signatures gallery — Steve 2026-08-05:
+// "make all white not black"). Every surface is a real BlurView refracting the
+// paper-white scene behind it, with a specular top edge so panels read as glass.
+// Android's BlurView needs the experimental method or it silently renders a slab.
+// NOTE: `ink` stays the dark museum charcoal — it is the TEXT color and the
+// text-on-gold color; `accentSoft` (once parchment used as light-on-dark text)
+// is now the dark warm body-text color so every screen inherits readable text.
 
 export const GLASS = {
   ink: '#141210',
-  inkDeep: '#0d0b0a',
-  emerald: '#2a2118',       // warm brown mid-tone for the scene gradient
-  accent: '#c9a86a',        // museum gold
-  accentSoft: '#e8d9b8',    // parchment
-  textDim: '#a89f90',
-  edge: 'rgba(255,255,255,0.26)',
-  edgeFaint: 'rgba(255,255,255,0.11)',
-  sheen: 'rgba(255,255,255,0.08)',
+  inkDeep: '#faf8f2',       // scene mid-tone (was near-black)
+  emerald: '#f1ebdd',       // warm paper tone closing the scene gradient
+  accent: '#9a7b3f',        // museum gold, deepened to read on white
+  accentSoft: '#2b241a',    // primary text — warm near-black on white
+  textDim: '#7a715f',       // muted text on white
+  edge: 'rgba(20,18,16,0.16)',
+  edgeFaint: 'rgba(20,18,16,0.09)',
+  sheen: 'rgba(255,255,255,0.65)',
 } as const;
 
 const blurProps = Platform.select({
@@ -32,7 +35,7 @@ const blurProps = Platform.select({
 export function GlassScene({ children, style }: { children: React.ReactNode; style?: ViewStyle }) {
   return (
     <LinearGradient
-      colors={[GLASS.ink, GLASS.inkDeep, GLASS.emerald]}
+      colors={['#ffffff', GLASS.inkDeep, GLASS.emerald]}
       locations={[0, 0.55, 1]}
       start={{ x: 0.1, y: 0 }}
       end={{ x: 0.9, y: 1 }}
@@ -40,7 +43,7 @@ export function GlassScene({ children, style }: { children: React.ReactNode; sty
     >
       {/* off-center warm-gold glow orb gives the blur a light source to bend */}
       <LinearGradient
-        colors={['rgba(201,168,106,0.26)', 'rgba(201,168,106,0)']}
+        colors={['rgba(201,168,106,0.18)', 'rgba(201,168,106,0)']}
         style={styles.orb}
         pointerEvents="none"
       />
@@ -51,7 +54,7 @@ export function GlassScene({ children, style }: { children: React.ReactNode; sty
 
 /** Frosted panel — the base glass slab (cards, hero, sections). */
 export function GlassPanel({
-  children, style, intensity = 28, tint = 'dark',
+  children, style, intensity = 28, tint = 'light',
 }: {
   children: React.ReactNode; style?: ViewStyle; intensity?: number;
   tint?: 'dark' | 'light';
@@ -74,7 +77,7 @@ export function GlassButton({
   return (
     <Pressable onPress={onPress} disabled={disabled || busy}
       style={({ pressed }) => [styles.btnClip, pressed && styles.btnPressed]}>
-      <BlurView intensity={solid ? 50 : 26} tint="dark" style={StyleSheet.absoluteFill} {...blurProps} />
+      <BlurView intensity={solid ? 50 : 26} tint="light" style={StyleSheet.absoluteFill} {...blurProps} />
       {solid && (
         <LinearGradient
           colors={['rgba(201,168,106,0.95)', 'rgba(176,142,84,0.92)']}
@@ -95,7 +98,7 @@ export function GlassPill({
 }: { children: React.ReactNode; tintColor?: string; style?: ViewStyle }) {
   return (
     <View style={[styles.pillClip, style]}>
-      <BlurView intensity={30} tint="dark" style={StyleSheet.absoluteFill} {...blurProps} />
+      <BlurView intensity={30} tint="light" style={StyleSheet.absoluteFill} {...blurProps} />
       {tintColor && <View style={[StyleSheet.absoluteFill, { backgroundColor: tintColor, opacity: 0.55 }]} />}
       <View style={styles.sheenTop} pointerEvents="none" />
       <View style={styles.pillBody}>{children}</View>
@@ -109,7 +112,7 @@ export function GlassBar({
 }: { title: string; onDone: () => void; light?: boolean }) {
   return (
     <View style={styles.barClip}>
-      <BlurView intensity={40} tint={light ? 'light' : 'dark'} style={StyleSheet.absoluteFill} {...blurProps} />
+      <BlurView intensity={40} tint="light" style={StyleSheet.absoluteFill} {...blurProps} />
       <View style={styles.sheenTop} pointerEvents="none" />
       <View style={styles.barRow}>
         <Pressable onPress={onDone} hitSlop={12}>
@@ -161,6 +164,6 @@ const styles = StyleSheet.create({
   },
   barDone: { color: GLASS.accent, fontSize: 16, fontWeight: '600', width: 44 },
   barDoneLight: { color: '#141210' },
-  barTitle: { fontSize: 16, fontWeight: '700', color: '#fff' },
+  barTitle: { fontSize: 16, fontWeight: '700', color: GLASS.ink },
   barTitleLight: { color: '#141210' },
 });

← 701c009 mobile: harden games port per adversarial review  ·  back to CelebritySignatures  ·  web: clicking a signature opens its details popup; Books sec e2d7d1c →