6 Commits

Author SHA1 Message Date
Kara Zajac 24a2dc83e5 Survivor side (1/4): safety guidance + first-run onboarding
Build APK / build (push) Has been cancelled
- OnboardingScreen: first-launch explainer (what it does, the offline promise, why it needs each permission), then requests permissions.
- SafetyScreen: 'if a tracker is following you' guidance — get safe, think before removing, document, report — with Call 911, the US DV Hotline (call / text START), and SPARC stalking resources. Every action hands off to the dialer/messages/browser; VIGIL stays offline.
- Entry points: a Safety icon in the top bar, and a red 'What should I do?' button on ALERTING/SUSPICIOUS trackers.
Release 0.1.8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 19:39:32 -04:00
Kara Zajac 1c0a6f216d Cache-bust style.css so the new .shots gallery rules load
Cloudflare was serving a stale cached style.css without the screenshots
gallery CSS; version the link (?v=2) to fetch the fresh stylesheet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173RhsXkdMViDR8DMNAG3ms
2026-07-15 19:17:17 -04:00
Kara Zajac a71a83891a Add screenshots gallery to the VIGIL showcase page
New "Screens" section (3-up phone gallery: active tracker list with
"It's mine" triage, locate-mode searching, and homing at -52 dBm /
ring), plus three optimized app screenshots, a nav anchor, and .shots CSS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173RhsXkdMViDR8DMNAG3ms
2026-07-15 19:12:21 -04:00
Kara Zajac b72faea8ce Add app screenshots to README
docs/screenshots/: main watch list, hot/cold finder (searching), and finder homing in with Make it ring. Added a Screenshots section to the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 19:09:08 -04:00
Kara Zajac ee13e10173 docs: cache-bust the main screenshot 2026-07-15 18:59:58 -04:00
Kara Zajac 51cd474573 docs: cleaner main-screen screenshot (no device chrome)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:59:18 -04:00
16 changed files with 381 additions and 36 deletions
+12
View File
@@ -28,6 +28,18 @@ that idea.
--- ---
## Screenshots
<p align="center">
<img src="docs/screenshots/main.png" width="230" alt="VIGIL watch list — whether anything has been following you" />
&nbsp;
<img src="docs/screenshots/finder-searching.png" width="230" alt="Hot/cold finder searching for a tracker" />
&nbsp;
<img src="docs/screenshots/finder-close.png" width="230" alt="Finder homing in on a device, with Make it ring" />
</p>
<p align="center"><sub>The watch list &nbsp;·&nbsp; the hot/cold finder searching &nbsp;·&nbsp; homing in on a tracker, with “Make it ring”</sub></p>
## Privacy first — fully offline ## Privacy first — fully offline
VIGIL requests **no `INTERNET` permission at all.** There is no server, no account, VIGIL requests **no `INTERNET` permission at all.** There is no server, no account,
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "org.soulstone.vigil" applicationId = "org.soulstone.vigil"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 8 versionCode = 9
versionName = "0.1.7" versionName = "0.1.8"
} }
// Fixed debug keystore committed to the repo (a debug key is non-secret — its // Fixed debug keystore committed to the repo (a debug key is non-secret — its
@@ -14,6 +14,8 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -24,6 +26,8 @@ import org.soulstone.vigil.ring.TrackerRinger
import org.soulstone.vigil.data.settings.Settings import org.soulstone.vigil.data.settings.Settings
import org.soulstone.vigil.service.ScanService import org.soulstone.vigil.service.ScanService
import org.soulstone.vigil.ui.MainScreen import org.soulstone.vigil.ui.MainScreen
import org.soulstone.vigil.ui.OnboardingScreen
import org.soulstone.vigil.ui.SafetyScreen
import org.soulstone.vigil.ui.theme.VigilTheme import org.soulstone.vigil.ui.theme.VigilTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@@ -68,36 +72,48 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
VigilTheme { VigilTheme {
val running by ScanService.running.collectAsState() val onboarded by settings.onboarded.collectAsState()
val trackers by repo.observeTrackers().collectAsState(initial = emptyList()) var showSafety by remember { mutableStateOf(false) }
val sensitivity by settings.sensitivity.collectAsState() when {
val granted by permissionsGranted !onboarded -> OnboardingScreen(onFinish = {
settings.setOnboarded(true)
if (!hasEssentialPermissions()) permissionLauncher.launch(requiredPermissions)
})
showSafety -> SafetyScreen(onBack = { showSafety = false })
else -> {
val running by ScanService.running.collectAsState()
val trackers by repo.observeTrackers().collectAsState(initial = emptyList())
val sensitivity by settings.sensitivity.collectAsState()
val granted by permissionsGranted
MainScreen( MainScreen(
running = running, running = running,
trackers = trackers, trackers = trackers,
sensitivity = sensitivity, sensitivity = sensitivity,
permissionMessage = if (granted) null permissionMessage = if (granted) null
else "Grant Bluetooth + location to start watching", else "Grant Bluetooth + location to start watching",
onStartStop = { onStartStop = {
if (running) { if (running) {
ScanService.stop(this) ScanService.stop(this)
} else if (granted) { } else if (granted) {
ScanService.start(this) ScanService.start(this)
} else { } else {
permissionLauncher.launch(requiredPermissions) permissionLauncher.launch(requiredPermissions)
} }
}, },
onSetSensitivity = { settings.setSensitivity(it) }, onSetSensitivity = { settings.setSensitivity(it) },
onApprove = { id, approved -> onApprove = { id, approved ->
lifecycleScope.launch { repo.setApproved(id, approved) } lifecycleScope.launch { repo.setApproved(id, approved) }
}, },
onDistrust = { id -> onDistrust = { id ->
lifecycleScope.launch { repo.clearBaseline(id) } lifecycleScope.launch { repo.clearBaseline(id) }
}, },
onRing = { tracker -> ringTracker(tracker) }, onRing = { tracker -> ringTracker(tracker) },
onClearAll = { lifecycleScope.launch { repo.clearAll() } } onClearAll = { lifecycleScope.launch { repo.clearAll() } },
) onOpenSafety = { showSafety = true }
)
}
}
} }
} }
} }
@@ -25,8 +25,16 @@ class Settings private constructor(context: Context) {
_sensitivity.value = value _sensitivity.value = value
} }
private val _onboarded = MutableStateFlow(prefs.getBoolean(KEY_ONBOARDED, false))
val onboarded: StateFlow<Boolean> = _onboarded.asStateFlow()
fun setOnboarded(value: Boolean) {
prefs.edit().putBoolean(KEY_ONBOARDED, value).apply()
_onboarded.value = value
}
companion object { companion object {
private const val KEY_SENSITIVITY = "sensitivity" private const val KEY_SENSITIVITY = "sensitivity"
private const val KEY_ONBOARDED = "onboarded"
@Volatile private var INSTANCE: Settings? = null @Volatile private var INSTANCE: Settings? = null
fun get(context: Context): Settings = INSTANCE ?: synchronized(this) { fun get(context: Context): Settings = INSTANCE ?: synchronized(this) {
@@ -32,6 +32,7 @@ import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.DeleteSweep import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.GppMaybe import androidx.compose.material.icons.filled.GppMaybe
import androidx.compose.material.icons.filled.HealthAndSafety
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Radar import androidx.compose.material.icons.filled.Radar
import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.filled.Shield
@@ -90,7 +91,8 @@ fun MainScreen(
onApprove: (String, Boolean) -> Unit, onApprove: (String, Boolean) -> Unit,
onDistrust: (String) -> Unit, onDistrust: (String) -> Unit,
onRing: (TrackerEntity) -> Unit, onRing: (TrackerEntity) -> Unit,
onClearAll: () -> Unit onClearAll: () -> Unit,
onOpenSafety: () -> Unit
) { ) {
var detail by remember { mutableStateOf<TrackerEntity?>(null) } var detail by remember { mutableStateOf<TrackerEntity?>(null) }
var finding by remember { mutableStateOf<TrackerEntity?>(null) } var finding by remember { mutableStateOf<TrackerEntity?>(null) }
@@ -119,6 +121,13 @@ fun MainScreen(
} }
}, },
actions = { actions = {
IconButton(onClick = onOpenSafety) {
Icon(
Icons.Filled.HealthAndSafety,
contentDescription = "Safety & help",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (trackers.isNotEmpty()) { if (trackers.isNotEmpty()) {
IconButton(onClick = onClearAll) { IconButton(onClick = onClearAll) {
Icon( Icon(
@@ -228,7 +237,8 @@ fun MainScreen(
onApprove = { id, a -> onApprove(id, a); detail = null }, onApprove = { id, a -> onApprove(id, a); detail = null },
onDistrust = { id -> onDistrust(id); detail = null }, onDistrust = { id -> onDistrust(id); detail = null },
onFind = { dev -> ScanService.setFinderTarget(dev.stableId); finding = dev; detail = null }, onFind = { dev -> ScanService.setFinderTarget(dev.stableId); finding = dev; detail = null },
onRing = onRing onRing = onRing,
onSafety = { onOpenSafety(); detail = null }
) )
} }
} }
@@ -378,7 +388,8 @@ private fun TrackerDetail(
onApprove: (String, Boolean) -> Unit, onApprove: (String, Boolean) -> Unit,
onDistrust: (String) -> Unit, onDistrust: (String) -> Unit,
onFind: (TrackerEntity) -> Unit, onFind: (TrackerEntity) -> Unit,
onRing: (TrackerEntity) -> Unit onRing: (TrackerEntity) -> Unit,
onSafety: () -> Unit
) { ) {
val status = statusOf(t) val status = statusOf(t)
Column(Modifier.fillMaxWidth().padding(24.dp)) { Column(Modifier.fillMaxWidth().padding(24.dp)) {
@@ -389,6 +400,18 @@ private fun TrackerDetail(
} }
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text(statusLabel(status), color = statusColor(status), fontWeight = FontWeight.SemiBold) Text(statusLabel(status), color = statusColor(status), fontWeight = FontWeight.SemiBold)
if (status == TrackerStatus.ALERTING || status == TrackerStatus.SUSPICIOUS) {
Spacer(Modifier.height(14.dp))
Button(
onClick = onSafety,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(containerColor = VigilRed, contentColor = Color(0xFF11111B))
) {
Icon(Icons.Filled.HealthAndSafety, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.size(8.dp))
Text("What should I do?", fontWeight = FontWeight.Bold)
}
}
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
DetailRow("Signal (last / peak)", "${t.lastRssi} / ${t.peakRssi} dBm") DetailRow("Signal (last / peak)", "${t.lastRssi} / ${t.peakRssi} dBm")
@@ -0,0 +1,87 @@
package org.soulstone.vigil.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Sensors
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import org.soulstone.vigil.R
/** First-run: what VIGIL does, its offline promise, and why it needs its permissions. */
@Composable
fun OnboardingScreen(onFinish: () -> Unit) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(28.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(32.dp))
Image(painterResource(R.drawable.vigil_logo), null, modifier = Modifier.size(76.dp))
Spacer(Modifier.height(14.dp))
Text("VIGIL", style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold)
Text(
"Know what's been following you.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(36.dp))
Point(
Icons.Filled.Sensors,
"Finds what follows you",
"Detects AirTags, Tile, Samsung SmartTags and Find My trackers that travel with you over time — not just whatever happens to be nearby."
)
Point(
Icons.Filled.Lock,
"Stays on your phone",
"No account, no internet, no telemetry. Everything VIGIL learns lives on this device and never leaves it."
)
Point(
Icons.Filled.LocationOn,
"Why it needs permissions",
"Bluetooth to hear trackers, location to tell one is moving with you across places, and notifications to warn you. That's the whole reason it asks."
)
Spacer(Modifier.height(32.dp))
Button(onClick = onFinish, modifier = Modifier.fillMaxWidth().height(52.dp)) {
Text("Get started", fontWeight = FontWeight.Bold)
}
Spacer(Modifier.height(24.dp))
}
}
}
@Composable
private fun Point(icon: ImageVector, title: String, body: String) {
Row(Modifier.fillMaxWidth().padding(vertical = 12.dp)) {
Icon(icon, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(26.dp).padding(top = 2.dp))
Spacer(Modifier.size(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(title, fontWeight = FontWeight.SemiBold)
Text(body, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
@@ -0,0 +1,135 @@
package org.soulstone.vigil.ui
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.Chat
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.soulstone.vigil.ui.theme.VigilRed
/** Survivor-centred guidance shown when a tracker may be following you. Every
* action hands off to the dialer / messages / browser — VIGIL stays offline. */
@Composable
fun SafetyScreen(onBack: () -> Unit) {
BackHandler(onBack = onBack)
val ctx = LocalContext.current
fun go(intent: Intent) = runCatching { ctx.startActivity(intent) }
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onBack) { Icon(Icons.Filled.ArrowBack, "Back") }
Spacer(Modifier.size(4.dp))
Text("If a tracker is following you", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
}
Spacer(Modifier.height(8.dp))
Text(
"VIGIL is a detection tool, not a substitute for professional help. If you're in immediate danger, call emergency services.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(20.dp))
Step(1, "Get somewhere safe", "If you feel in danger, head to a public place or someone you trust before doing anything else.")
Step(2, "Think before you remove it", "Turning a tracker off can alert whoever placed it and escalate the risk. If you might be in danger, consider documenting it and getting help first.")
Step(3, "Document what you can", "Save when and where it was seen (use Export), and photograph the device if you find it. A dated record helps police and advocates.")
Step(4, "Report it", "Contact local police — 911 if you're in immediate danger. If this involves a partner or ex, a domestic-violence advocate can help you make a safety plan.")
Spacer(Modifier.height(24.dp))
Text("Get help", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, letterSpacing = 1.sp)
Spacer(Modifier.height(10.dp))
Button(
onClick = { go(Intent(Intent.ACTION_DIAL, Uri.parse("tel:911"))) },
modifier = Modifier.fillMaxWidth().height(50.dp),
colors = ButtonDefaults.buttonColors(containerColor = VigilRed, contentColor = Color(0xFF11111B))
) {
Icon(Icons.Filled.Call, null); Spacer(Modifier.size(8.dp)); Text("Call 911 (emergency)", fontWeight = FontWeight.Bold)
}
Spacer(Modifier.height(10.dp))
Card(Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)) {
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text("U.S. National Domestic Violence Hotline", fontWeight = FontWeight.SemiBold)
Text("Free, confidential, 24/7 — help with stalking and abuse, including safety planning.",
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { go(Intent(Intent.ACTION_DIAL, Uri.parse("tel:18007997233"))) }, modifier = Modifier.weight(1f)) {
Icon(Icons.Filled.Call, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.size(6.dp)); Text("Call")
}
OutlinedButton(onClick = { go(Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:88788")).putExtra("sms_body", "START")) }, modifier = Modifier.weight(1f)) {
Icon(Icons.Filled.Chat, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.size(6.dp)); Text("Text START")
}
}
}
}
Spacer(Modifier.height(10.dp))
OutlinedButton(
onClick = { go(Intent(Intent.ACTION_VIEW, Uri.parse("https://www.stalkingawareness.org/help-for-victims/"))) },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Filled.OpenInNew, null, modifier = Modifier.size(18.dp)); Spacer(Modifier.size(8.dp)); Text("Stalking help & resources (SPARC)")
}
Spacer(Modifier.height(16.dp))
Text(
"These open your phone, messages, or browser — VIGIL itself never connects to the internet. Resources shown are U.S.; check what's available where you are.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(24.dp))
}
}
}
@Composable
private fun Step(n: Int, title: String, body: String) {
Row(Modifier.fillMaxWidth().padding(vertical = 9.dp)) {
Box(
Modifier.size(26.dp).clip(CircleShape).background(MaterialTheme.colorScheme.primary),
contentAlignment = Alignment.Center
) { Text("$n", color = MaterialTheme.colorScheme.onPrimary, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.labelMedium) }
Spacer(Modifier.size(14.dp))
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(title, fontWeight = FontWeight.SemiBold)
Text(body, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+34 -2
View File
@@ -21,7 +21,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css?v=2">
</head> </head>
<body> <body>
@@ -40,6 +40,7 @@
<div class="nav-links"> <div class="nav-links">
<a href="#detects">What it detects</a> <a href="#detects">What it detects</a>
<a href="#decides">How it decides</a> <a href="#decides">How it decides</a>
<a href="#screens">Screens</a>
<a href="#clones">Clones</a> <a href="#clones">Clones</a>
<a href="#download">Download</a> <a href="#download">Download</a>
<a class="nav-github" href="https://github.com/KaraZajac/VIGIL" aria-label="GitHub"> <a class="nav-github" href="https://github.com/KaraZajac/VIGIL" aria-label="GitHub">
@@ -69,7 +70,7 @@
</p> </p>
<div class="phone-hero"> <div class="phone-hero">
<img class="phone" src="img/vigil-main.png" width="1096" height="2560" <img class="phone" src="img/vigil-main.png?v=2" width="1096" height="2280"
alt="VIGIL main screen: a green &quot;You're clear — nothing has been following you&quot; card, a Stop Watching button, and a High/Medium/Low sensitivity selector." loading="eager"> alt="VIGIL main screen: a green &quot;You're clear — nothing has been following you&quot; card, a Stop Watching button, and a High/Medium/Low sensitivity selector." loading="eager">
</div> </div>
@@ -223,6 +224,37 @@
</div> </div>
</section> </section>
<!-- ──────────────── SCREENSHOTS ──────────────── -->
<section id="screens" class="section reveal">
<div class="container">
<div class="section-head">
<span class="section-tag">screenshots</span>
<h2>Spot it, then walk it down.</h2>
<p class="lead">No account, no map tiles, no cloud — it all runs on the
phone. See what's moving with you, wave off your own gear, and when
something's left over, home in on it.</p>
</div>
<div class="shots">
<figure class="shot">
<img class="phone" src="img/vigil-active.png" width="1096" height="2560"
alt="VIGIL active list: an ACTIVE (6) list of nearby Apple Find My and Google Find My Device trackers, each showing places seen, signal in dBm, time since last seen, and an It's-mine button." loading="lazy">
<figcaption><strong>The active list</strong>Every tracker seen moving with you — signal, places, and how long ago. Tap &ldquo;It's mine&rdquo; and your own gear drops out.</figcaption>
</figure>
<figure class="shot">
<img class="phone" src="img/vigil-locate.png" width="1096" height="2560"
alt="VIGIL locate screen for an Apple Find My tracker: a large pulsing radar icon reading Searching… walk around, no signal yet, with a Make-it-ring button." loading="lazy">
<figcaption><strong>Locate mode</strong>Pick a suspect and walk — the ripple strengthens as you close in. No signal yet? Keep moving.</figcaption>
</figure>
<figure class="shot">
<img class="phone" src="img/vigil-signal.png" width="1096" height="2560"
alt="VIGIL locate screen for a Google Find My Device tracker reading Close, minus 52 dBm, with a Make-it-ring button and a Ringing… listen for the tracker toast." loading="lazy">
<figcaption><strong>Getting warmer</strong>Live signal strength as you approach — &minus;52 dBm is close. &ldquo;Make it ring&rdquo; forces a silent tracker to give itself up.</figcaption>
</figure>
</div>
</div>
</section>
<!-- ──────────────── CLONES ──────────────── --> <!-- ──────────────── CLONES ──────────────── -->
<section id="clones" class="section section-feature reveal"> <section id="clones" class="section section-feature reveal">
<div class="container"> <div class="container">
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

+32
View File
@@ -1089,3 +1089,35 @@ pre.code .prompt { color: var(--accent-hot); user-select: none; }
/* VIGIL: logo mark in nav/footer (img instead of a glyph) */ /* VIGIL: logo mark in nav/footer (img instead of a glyph) */
.nav-logo { display: inline-block; vertical-align: middle; border-radius: 7px; } .nav-logo { display: inline-block; vertical-align: middle; border-radius: 7px; }
.nav-brand .nav-logo { margin-right: 3px; } .nav-brand .nav-logo { margin-right: 3px; }
/* ============================================================
SCREENSHOTS GALLERY
============================================================ */
.shots {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2.6rem;
margin-top: 2.8rem;
align-items: start;
}
.shots .shot { margin: 0; display: flex; flex-direction: column; align-items: center; }
.shots .phone { width: 260px; max-width: 100%; }
.shots figcaption {
margin-top: 1.4rem;
max-width: 260px;
color: var(--text-muted);
font-size: 0.9rem;
line-height: 1.55;
text-align: center;
}
.shots figcaption strong {
display: block;
color: var(--text);
font-family: var(--display);
font-size: 1rem;
margin-bottom: 0.3rem;
}
@media (max-width: 820px) {
.shots { grid-template-columns: 1fr; gap: 3rem; }
.shots .phone { width: 256px; }
}