diff --git a/mobile/src/components/GoonWordmark.tsx b/mobile/src/components/GoonWordmark.tsx
new file mode 100644
index 0000000..cc5b1d1
--- /dev/null
+++ b/mobile/src/components/GoonWordmark.tsx
@@ -0,0 +1,124 @@
+/**
+ * Goon wordmark — custom letterform, NOT a typed text in a font.
+ *
+ * 4 litery jako path geometry. Detale:
+ * - "g" otwarte u dołu (descender curve cut), ucho descender przedłużone
+ * - "o" — okrąg z lekkim spłaszczeniem w pionie (1.0×0.95 aspect), counter
+ * z 60% wysokości (cięższe niż typical grotesk → daje "weight")
+ * - "n" — minimal grotesk, stem łączy się z arc bez serif joint
+ * - Kerning naturalny, ale "oo" lekko ciaśniejsze niż "go"/"on"
+ *
+ * Viewbox 200×56 — 4 litery × ~44px width, używamy aspect ratio scale.
+ *
+ * Użycie:
+ *
+ * // dla splash
+ */
+import React from 'react';
+import Svg, { Path } from 'react-native-svg';
+
+interface Props {
+ width?: number;
+ color?: string;
+}
+
+export function GoonWordmark({ width = 120, color = '#F5EDE0' }: Props) {
+ const height = width * (56 / 200);
+ return (
+
+ );
+}
+
+/**
+ * Monogram — tylko "g" + ucho descender, jako standalone icon.
+ * 56×56, używany jako adaptive-icon / splash mark.
+ */
+export function GoonMark({ size = 48, color = '#F5EDE0' }: { size?: number; color?: string }) {
+ return (
+
+ );
+}
diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx
index cbee240..deb1b0a 100644
--- a/mobile/src/navigation.tsx
+++ b/mobile/src/navigation.tsx
@@ -4,6 +4,7 @@ import {
useNavigationContainerRef,
} from '@react-navigation/native';
import { BugReportFAB } from './components/BugReportFAB';
+import { GoonWordmark } from './components/GoonWordmark';
import { GoonClient } from './api';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
@@ -88,7 +89,10 @@ function TopTabs({
}) {
const tabs: TopTab[] = ['Scenes', 'Movies', 'Sites'];
return (
-
+
+ {/* Wordmark — branding po lewej, dystans 14px do pierwszego tabu. */}
+
+
{tabs.map((t) => {
const active = t === current;
return (
@@ -96,8 +100,9 @@ function TopTabs({
{t}
diff --git a/mobile/src/screens/ScenesScreen.tsx b/mobile/src/screens/ScenesScreen.tsx
index 62fde60..1182bfe 100644
--- a/mobile/src/screens/ScenesScreen.tsx
+++ b/mobile/src/screens/ScenesScreen.tsx
@@ -118,7 +118,9 @@ export function ScenesScreen() {
s.id}
- renderItem={({ item }) => }
+ numColumns={2}
+ renderItem={({ item }) => }
+ columnWrapperStyle={styles.gridRow}
ListHeaderComponent={!debouncedQ && activeCount === 0 ? : null}
refreshing={isRefetching}
onRefresh={refetch}
@@ -221,6 +223,80 @@ function FavoritesButton() {
);
}
+/**
+ * SceneTile — 2-col 16:9 grid item, minimal text (video portal).
+ *
+ * Per impeccable.style/slop + Jan feedback "większe miniaturki, mniej tekstu":
+ * - Thumb wypełnia szerokość kolumny (16:9 aspect ratio)
+ * - Title 1 linijka, system font, weight 600
+ * - Overlay: studio (mono small) + duration → tylko na thumb, NIE pod
+ * - Wyrzucone: performers, release_date, sources count, "✓ watched" string
+ * (zastąpione check badge na thumb)
+ * - Long-press → animated preview (zachowane)
+ */
+function SceneTile({ scene }: { scene: SceneOut }) {
+ const navigation =
+ useNavigation>();
+ const [isPreviewing, setIsPreviewing] = useState(false);
+
+ const animatedUrl = scene.playback_sources.find((s) => s.animated_thumbnail_url)
+ ?.animated_thumbnail_url;
+ const staticUrl = scene.playback_sources.find((s) => s.thumbnail_url)?.thumbnail_url;
+ const displayUrl = isPreviewing && animatedUrl ? animatedUrl : staticUrl ?? animatedUrl;
+
+ const startPreview = () => {
+ if (!animatedUrl) return;
+ setIsPreviewing(true);
+ Haptics.selectionAsync().catch(() => {});
+ };
+
+ const dim = scene.finished === true;
+ const dur = scene.duration_sec;
+ const durLabel =
+ dur && dur > 0
+ ? dur >= 3600
+ ? `${Math.floor(dur / 3600)}h${String(Math.floor((dur % 3600) / 60)).padStart(2, '0')}`
+ : `${Math.floor(dur / 60)}m`
+ : null;
+
+ return (
+ navigation.navigate('SceneDetail', { id: scene.id })}
+ onLongPress={startPreview}
+ onPressOut={() => setIsPreviewing(false)}
+ delayLongPress={180}
+ >
+
+
+ {scene.is_favorite ? (
+
+ ★
+
+ ) : null}
+ {durLabel ? (
+
+ {durLabel}
+
+ ) : null}
+ {dim ? (
+
+ ✓
+
+ ) : null}
+
+
+ {scene.title}
+
+ {scene.studio?.name ? (
+
+ {scene.studio.name}
+
+ ) : null}
+
+ );
+}
+
function SceneRow({ scene }: { scene: SceneOut }) {
const navigation =
useNavigation>();
@@ -429,4 +505,72 @@ const styles = StyleSheet.create({
marginTop: 6,
lineHeight: 15,
},
+
+ // 2-col grid (SceneTile) — wprowadzone 2026-05-29 (UI overhaul, Jan feedback
+ // "większe miniaturki, mniej tekstu"). Wcześniej był full-width SceneRow z 6
+ // liniami tekstu — sloppy density dla video portalu.
+ gridRow: { gap: 10, marginBottom: 14 },
+ tile: { flex: 1 },
+ tileThumbWrap: {
+ width: '100%',
+ aspectRatio: 16 / 9,
+ borderRadius: 6,
+ overflow: 'hidden',
+ position: 'relative',
+ backgroundColor: theme.bgElevated,
+ },
+ tileThumb: { width: '100%', height: '100%' },
+ tileThumbDim: { opacity: 0.45 },
+ tileFavBadge: {
+ position: 'absolute',
+ top: 6,
+ left: 6,
+ backgroundColor: 'rgba(0,0,0,0.7)',
+ paddingHorizontal: 5,
+ paddingVertical: 1,
+ borderRadius: 6,
+ },
+ tileFavBadgeText: { color: theme.accent, fontSize: 11, fontWeight: '700' },
+ tileDurBadge: {
+ position: 'absolute',
+ bottom: 6,
+ right: 6,
+ backgroundColor: 'rgba(0,0,0,0.78)',
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ borderRadius: 4,
+ },
+ tileDurText: {
+ color: theme.fg,
+ fontSize: 11,
+ fontWeight: '600',
+ fontVariant: ['tabular-nums'],
+ },
+ tileWatchedBadge: {
+ position: 'absolute',
+ top: 6,
+ right: 6,
+ backgroundColor: 'rgba(0,0,0,0.78)',
+ width: 20,
+ height: 20,
+ borderRadius: 999,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ tileWatchedText: { color: theme.fg, fontSize: 11, fontWeight: '700' },
+ tileTitle: {
+ color: theme.fg,
+ fontSize: 14,
+ fontWeight: '600',
+ marginTop: 8,
+ letterSpacing: -0.2,
+ },
+ tileTitleDim: { color: theme.muted },
+ tileStudio: {
+ color: theme.muted,
+ fontSize: 11,
+ marginTop: 2,
+ letterSpacing: 0.3,
+ textTransform: 'uppercase',
+ },
});
diff --git a/mobile/src/theme.ts b/mobile/src/theme.ts
index df39539..596d0b8 100644
--- a/mobile/src/theme.ts
+++ b/mobile/src/theme.ts
@@ -1,26 +1,100 @@
+/**
+ * Goon theme — warm dark + oxblood, NOT generic navy/purple AI-default.
+ *
+ * Audit 2026-05-29 (impeccable.style/slop):
+ * - Wcześniej: bg #08090F (deep navy), accent #8B5CF6 (purple) + glow #A78BFA
+ * → literal example "AI default palette" z impeccable.
+ * - Teraz: bg #15110D (warm charcoal — orange undertone), accent #B23A48
+ * (oxblood/rust), brak glowów, brak gradientów neon.
+ *
+ * Typography: General Sans (display, Fontshare) + Geist Mono (meta, Vercel
+ * fonts). Font files w `mobile/assets/fonts/` — `useFonts()` w App.tsx
+ * graceful-fallback do system gdy brak (development).
+ */
+
export const theme = {
- bg: '#08090F',
- bgElevated: '#11131C',
- card: '#1A1D2A',
- border: '#262A3D',
- borderFocus: '#8B5CF6',
+ // Warm dark — charcoal z orange undertone (NIE navy).
+ // Filmowy, premium feel — jak Letterboxd/Mubi/A24.
+ bg: '#15110D',
+ bgElevated: '#1E1A14',
+ card: '#26201A',
+ border: '#3A3128',
+ borderFocus: '#B23A48',
- fg: '#F4F4F8',
- muted: '#9CA0B5',
- mutedDim: '#6B6F85',
+ // Foreground — warm off-white (nie pure white żeby nie biło na warm dark).
+ fg: '#F5EDE0',
+ muted: '#A89B85',
+ mutedDim: '#6F6555',
- accent: '#8B5CF6',
- accentGlow: '#A78BFA',
- accentDeep: '#5B21B6',
- accentSecondary: '#3B82F6',
+ // Accent — oxblood/rust. Distinctive, niekonwencjonalne dla "media app".
+ // Brak glow/neon. Deep wariant dla pressed states.
+ accent: '#B23A48',
+ accentGlow: '#B23A48', // alias — back-compat, ale bez prawdziwego glow
+ accentDeep: '#7A1F2A',
+ accentSecondary: '#D89B4A', // muted amber dla secondary CTAs
- good: '#10B981',
- bad: '#EF4444',
- warn: '#F59E0B',
-};
+ // Status — tonowane do palety (nie generic green/red).
+ good: '#5E8C5A', // muted olive-green
+ bad: '#C44545',
+ warn: '#D89B4A', // amber
+
+} as const;
+
+/**
+ * Font family — pusty string = system default na iOS/Android (uniknięcie
+ * "font not found" warningów). Po zainstalowaniu custom fontów (TODO niżej):
+ * 1. `npx expo install expo-font expo-splash-screen`
+ * 2. Pobierz fonty:
+ * - General Sans (Fontshare, free): https://www.fontshare.com/fonts/general-sans
+ * - Geist Mono (Vercel, free OFL): https://github.com/vercel/geist-font
+ * 3. Skopiuj .ttf do `mobile/assets/fonts/`
+ * 4. W App.tsx:
+ * const [loaded] = useFonts({
+ * 'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'),
+ * 'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'),
+ * 'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'),
+ * });
+ * if (!loaded) return null; // lub SplashScreen.preventAutoHideAsync()
+ * 5. Zmień stałe poniżej na konkretne fontFamily strings.
+ *
+ * Do tego momentu: distinct hierarchy przez `type` scale + fontWeight w
+ * komponentach. System font (San Francisco / Roboto) jest OK temporary.
+ */
+export const fonts = {
+ display: undefined as string | undefined,
+ displayRegular: undefined as string | undefined,
+ mono: undefined as string | undefined,
+} as const;
export function scoreColor(score: number): string {
if (score >= 0.92) return theme.good;
if (score >= 0.75) return theme.warn;
return theme.bad;
}
+
+/**
+ * Type-scale (impeccable: "at least 1.25 ratio between steps").
+ * Base 14 → 17 → 22 → 28 → 36. Użyj zamiast hardcodowanego fontSize.
+ */
+export const type = {
+ micro: 11,
+ meta: 13,
+ body: 14,
+ bodyLarge: 17,
+ title: 22,
+ display: 28,
+ hero: 36,
+} as const;
+
+/**
+ * Spacing scale — "tight groupings, generous between sections" (impeccable).
+ * Pomijamy 12/20 — preferuj 8/16/24/32 dla widocznej hierarchii sekcji.
+ */
+export const space = {
+ xs: 4,
+ sm: 8,
+ md: 16,
+ lg: 24,
+ xl: 32,
+ xxl: 48,
+} as const;