initial commit
All checks were successful
pedestrian-simulator / build (push) Successful in 53s

This commit is contained in:
2026-01-11 17:16:59 -07:00
parent c5d00dc9a9
commit 24ecddd034
18 changed files with 4506 additions and 4 deletions

1517
frontend/app.js Normal file

File diff suppressed because it is too large Load Diff

200
frontend/index.html Normal file
View File

@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pedestrian Simulator</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<!-- Header with stats -->
<div id="header">
<div class="logo-section">
<h1>步行 Pedestrian Simulator</h1>
</div>
<!-- User Menu -->
<div id="user-menu" class="user-menu" style="display: none;">
<img id="userAvatar" class="user-avatar" src="" alt="User" />
<span id="userName" class="user-name"></span>
<button id="kmlBrowserButton" class="icon-button" title="Browse KML Files">📂</button>
<button id="logoutButton" class="icon-button" title="Logout">🚪</button>
</div>
<div id="stats-bar">
<!-- Unified Status Bar -->
<div class="stat-group clickable" id="routeInfoGroup">
<span class="stat-label">Route</span>
<span class="stat-value route-text" id="routeInfo">Not Started</span>
<span class="edit-icon" title="Reset/Change Route"></span>
</div>
<div class="divider"></div>
<div class="stat-group">
<span class="stat-label">Steps</span>
<span class="stat-value" id="currentSteps">0</span>
</div>
<div class="divider"></div>
<div class="stat-group">
<span class="stat-label">Distance</span>
<span class="stat-value" id="tripMeter">0.0 km</span>
</div>
<div class="divider"></div>
<div class="stat-group">
<span class="stat-label">Next Sync</span>
<div class="stat-row">
<span class="stat-value" id="nextSync">--:--</span>
<button id="refreshButton" class="icon-button" title="Refresh Now"></button>
</div>
</div>
</div>
</div>
<!-- Street View Container -->
<div id="streetview-container">
<div id="panorama"></div>
<div id="main-map" class="hidden-mode"></div>
<!-- Minimap -->
<div id="minimap-container">
<div id="minimap"></div>
</div>
<!-- Start Location Setup Overlay -->
<div id="setup-overlay" class="overlay active">
<div class="setup-card">
<h2>🌍 Plan Your Route</h2>
<p>Enter a start and finish location for your walk</p>
<div class="input-group">
<label>Start Location</label>
<input type="text" id="startLocationInput" placeholder="e.g., Central Park, NY" />
</div>
<div class="input-group">
<label>Destination</label>
<input type="text" id="endLocationInput" placeholder="e.g., Empire State Building, NY" />
</div>
<button id="startButton" class="primary-btn">Start Journey</button>
<div class="separator">
<span>OR</span>
</div>
<div class="custom-route-section">
<h3>📂 Use Saved Route</h3>
<p class="subtext">Browse and select from your uploaded KML files</p>
<button id="browseKmlButton" class="secondary-btn">Browse KML Files</button>
</div>
<p class="api-note">Note: You'll need a Google Maps API key configured</p>
</div>
</div>
<!-- API Key Setup Overlay -->
<div id="apikey-overlay" class="overlay active">
<div class="setup-card">
<h2>🔑 Google Maps API Key Required</h2>
<p>Enter your Google Maps API key to use Street View</p>
<input type="text" id="apiKeyInput" placeholder="Your Google Maps API Key" />
<button id="saveApiKeyButton" class="primary-btn">Save & Continue</button>
<p class="help-text">
<a href="https://developers.google.com/maps/documentation/javascript/get-api-key"
target="_blank">
How to get an API key →
</a>
</p>
</div>
</div>
<div id="app-footer">
<a href="privacy.html" target="_blank">Privacy Policy</a>
</div>
</div>
<!-- Login Overlay -->
<div id="login-overlay" class="overlay">
<div class="setup-card">
<h2>🔐 Login Required</h2>
<p>Please login with your Fitbit account to continue</p>
<button id="fitbitLoginButton" class="primary-btn">Login with Fitbit</button>
<div class="privacy-link-container">
<a href="privacy.html" target="_blank" class="secondary-link">Privacy Policy</a>
</div>
</div>
</div>
<!-- KML Browser Modal -->
<div id="kml-browser-overlay" class="overlay">
<div class="kml-browser-card">
<div class="kml-browser-header">
<h2>📂 KML File Browser</h2>
<button id="closeKmlBrowser" class="close-btn">×</button>
</div>
<div class="kml-tabs">
<button class="kml-tab active" data-tab="my-files">My Files</button>
<button class="kml-tab" data-tab="public-files">Public Files</button>
</div>
<div class="kml-tab-content">
<!-- My Files Tab -->
<div id="my-files-tab" class="tab-pane active">
<div class="upload-section">
<input type="file" id="kmlUploadInput" accept=".kml,.xml" style="display: none;" />
<button id="uploadKmlButton" class="primary-btn">📤 Upload KML File</button>
</div>
<div class="sort-controls">
<label>Sort by:</label>
<select id="myFilesSortSelect">
<option value="date">Newest First</option>
<option value="distance">Distance</option>
</select>
</div>
<div id="myFilesList" class="kml-file-list">
<p class="empty-message">No KML files uploaded yet</p>
</div>
</div>
<!-- Public Files Tab -->
<div id="public-files-tab" class="tab-pane">
<div class="sort-controls">
<label>Sort by:</label>
<select id="publicFilesSortSelect">
<option value="votes">Highest Votes</option>
<option value="date">Most Recent</option>
</select>
</div>
<div id="publicFilesList" class="kml-file-list">
<p class="empty-message">No public KML files available</p>
</div>
</div>
</div>
</div>
</div>
<!-- General Confirmation Overlay -->
<div id="confirm-overlay" class="overlay">
<div class="setup-card">
<h2 id="confirmTitle">⚠️ Confirm</h2>
<p id="confirmMessage">Are you sure you want to proceed?</p>
<div style="display: flex; gap: 1rem; width: 100%;">
<button id="generalConfirmButton" class="primary-btn" style="flex: 1;">Yes</button>
<button id="generalCancelButton" class="control-btn" style="flex: 1;">Cancel</button>
</div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

170
frontend/privacy.html Normal file
View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - Pedestrian Simulator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #6366f1;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--border: #334155;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
color: var(--text-primary);
line-height: 1.6;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
}
.container {
max-width: 800px;
padding: 4rem 2rem;
}
header {
margin-bottom: 3rem;
text-align: center;
}
h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--primary), #8b5cf6);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.back-link {
color: var(--primary);
text-decoration: none;
font-weight: 500;
display: inline-block;
margin-bottom: 2rem;
}
.back-link:hover {
text-decoration: underline;
}
section {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
}
h2 {
color: var(--primary);
margin-top: 0;
font-size: 1.5rem;
}
p {
color: var(--text-secondary);
}
ul {
color: var(--text-secondary);
padding-left: 1.5rem;
}
li {
margin-bottom: 0.5rem;
}
footer {
text-align: center;
color: var(--text-secondary);
font-size: 0.9rem;
margin-top: 4rem;
}
</style>
</head>
<body>
<div class="container">
<a href="/" class="back-link">← Back to App</a>
<header>
<h1>Privacy Policy</h1>
<p>Effective Date: January 11, 2026</p>
</header>
<section>
<h2>Introduction</h2>
<p>Welcome to Pedestrian Simulator. We are committed to protecting your privacy and providing a transparent
experience. This policy explains how we handle your data.</p>
</section>
<section>
<h2>Information We Collect</h2>
<p>To provide the experience of a walking simulator synchronized with your fitness tracker, we collect the
following information from your Fitbit account:</p>
<ul>
<li><strong>Profile Information:</strong> Your display name and avatar, used to personalize your
interface.</li>
<li><strong>Activity Data:</strong> Your daily step counts, which are used to determine your position
along your walking routes.</li>
<li><strong>KML Files:</strong> Any route files you choose to upload to the service.</li>
</ul>
</section>
<section>
<h2>How We Use Your Data</h2>
<p>Your data is used exclusively to facilitate the core functions of the application:</p>
<ul>
<li>Calculating progress along your selected routes.</li>
<li>Saving your trip state so you can resume where you left off.</li>
<li>Displaying your identity in the user menu.</li>
</ul>
</section>
<section>
<h2>No Third-Party Sharing</h2>
<p><strong>We do not share, sell, or trade your personal information with any third parties.</strong> All
data collected is stored securely on our server and is used ONLY for the operation of Pedestrian
Simulator.</p>
</section>
<section>
<h2>KML File Privacy</h2>
<p>By default, every KML file you upload is <strong>private</strong>. Only you can see it and use it for
your trips.</p>
<p>You have the option to make a file <strong>public</strong>. This is strictly an opt-in process. When a
file is public:</p>
<ul>
<li>Other users can see the route name and your display name.</li>
<li>Other users can use the route for their own trips.</li>
<li>Other users can vote on the quality of the route.</li>
</ul>
<p>You can toggle a file back to private or delete it at any time.</p>
</section>
<section>
<h2>Data Retention</h2>
<p>We retain your data as long as your account is active. You can delete your uploaded routes at any time.
If you wish to have all your user data removed from our systems, please contact the administrator.</p>
</section>
<footer>
&copy; 2026 Pedestrian Simulator. Built with privacy in mind.
</footer>
</div>
</body>
</html>

813
frontend/style.css Normal file
View File

@@ -0,0 +1,813 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--secondary: #8b5cf6;
--success: #10b981;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--border: #334155;
--shadow: rgba(0, 0, 0, 0.3);
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, var(--bg-dark) 0%, #1a1f3a 100%);
color: var(--text-primary);
overflow: hidden;
height: 100vh;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
/* Header Styles */
#header {
background: var(--bg-card);
padding: 0.5rem 1.5rem;
border-bottom: 2px solid var(--border);
box-shadow: 0 4px 6px var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
height: 70px;
}
.logo-section h1 {
font-size: 1.25rem;
font-weight: 700;
white-space: nowrap;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
#app-footer {
position: absolute;
bottom: 0.5rem;
left: 50%;
transform: translateX(-50%);
font-size: 0.75rem;
color: var(--text-secondary);
opacity: 0.6;
z-index: 10;
}
#app-footer a {
color: var(--text-secondary);
text-decoration: none;
}
#app-footer a:hover {
color: var(--primary);
text-decoration: underline;
}
/* User Menu */
.user-menu {
display: flex;
align-items: center;
gap: 0.75rem;
background: rgba(15, 23, 42, 0.4);
padding: 0.5rem 1rem;
border-radius: 12px;
border: 1px solid var(--border);
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--primary);
}
.user-name {
font-size: 0.9rem;
font-weight: 500;
color: var(--text-primary);
}
#stats-bar {
display: flex;
align-items: center;
background: rgba(15, 23, 42, 0.4);
border-radius: 12px;
padding: 0.25rem 0.5rem;
border: 1px solid var(--border);
flex: 1;
justify-content: flex-end;
}
.stat-group {
display: flex;
flex-direction: column;
padding: 0.25rem 1rem;
position: relative;
min-width: 100px;
}
.stat-group.clickable {
cursor: pointer;
background: rgba(99, 102, 241, 0.1);
border-radius: 8px;
transition: all 0.2s ease;
padding-right: 2rem;
flex: 1;
min-width: 200px;
}
.stat-group.clickable:hover {
background: rgba(99, 102, 241, 0.2);
box-shadow: 0 0 10px rgba(99, 102, 241, 0.1);
}
.stat-group.clickable:hover .edit-icon {
opacity: 1;
transform: translateY(-50%) scale(1.1);
}
.stat-label {
font-size: 0.65rem;
text-transform: uppercase;
color: var(--text-secondary);
letter-spacing: 0.05em;
margin-bottom: 0.1rem;
}
.stat-value {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.divider {
width: 1px;
height: 30px;
background: var(--border);
margin: 0 0.5rem;
}
.edit-icon {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 1.2rem;
color: var(--primary);
opacity: 0.5;
transition: all 0.2s ease;
}
.route-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 400px;
color: var(--primary);
}
/* Street View Container */
#streetview-container {
flex: 1;
position: relative;
overflow: hidden;
}
#panorama {
width: 100%;
height: 100%;
}
#main-map {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 5;
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: bottom right;
}
.hidden-mode {
display: none !important;
}
/* Minimap */
#minimap-container {
position: absolute;
bottom: 20px;
right: 20px;
width: 250px;
height: 200px;
z-index: 10;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
border: 3px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
#minimap-container:hover {
transform: scale(1.05);
border-color: var(--primary);
}
#minimap {
width: 100%;
height: 100%;
background: var(--bg-card);
}
/* Overlay Styles */
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(10px);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.overlay.active {
display: flex;
}
.setup-card {
background: var(--bg-card);
padding: 3rem;
border-radius: 16px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
max-width: 500px;
width: 90%;
border: 1px solid var(--border);
}
.setup-card h2 {
font-size: 1.75rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.setup-card p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.setup-card input[type="text"] {
width: 100%;
padding: 1rem;
background: var(--bg-dark);
border: 2px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
margin-bottom: 1.5rem;
transition: all 0.3s ease;
}
.setup-card input[type="text"]:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.input-group {
margin-bottom: 1.5rem;
}
.input-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-secondary);
font-weight: 500;
}
.input-group input {
width: 100%;
padding: 1rem;
background: var(--bg-dark);
border: 2px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
transition: all 0.3s ease;
}
.input-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.primary-btn {
width: 100%;
padding: 1rem 2rem;
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.primary-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4);
}
.primary-btn:active {
transform: translateY(0);
}
.api-note,
.help-text {
font-size: 0.875rem;
color: var(--text-secondary);
margin-top: 1rem;
text-align: center;
}
.help-text a {
color: var(--primary);
text-decoration: none;
}
.help-text a:hover {
text-decoration: underline;
}
.route-text {
cursor: pointer;
text-decoration: underline;
text-decoration-style: dotted;
text-underline-offset: 4px;
}
.route-text:hover {
color: var(--secondary);
}
.control-btn {
padding: 0.75rem 1.5rem;
background: var(--bg-dark);
border: 2px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
}
.control-btn:hover {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
/* Responsive Design */
@media (max-width: 768px) {
#header {
flex-direction: column;
height: auto;
padding: 0.5rem;
gap: 0.5rem;
}
.logo-section h1 {
font-size: 1rem;
}
#stats-bar {
width: 100%;
overflow-x: auto;
justify-content: flex-start;
}
.stat-group {
min-width: 80px;
padding: 0.25rem 0.5rem;
}
.stat-group.clickable {
min-width: 150px;
}
.setup-card {
padding: 2rem 1.5rem;
}
}
/* Loading animation */
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.loading {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Refresh Button Styles */
.stat-row {
display: flex;
align-items: center;
gap: 8px;
}
.icon-button {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
font-size: 1.2rem;
cursor: pointer;
padding: 0;
transition: all 0.2s ease;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.icon-button:hover {
color: #fff;
background: rgba(255, 255, 255, 0.1);
transform: rotate(30deg);
}
.icon-button:active {
transform: scale(0.9);
}
.icon-button.spinning {
animation: spin 1s linear infinite;
pointer-events: none;
opacity: 0.5;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Custom Route UI */
.separator {
display: flex;
align-items: center;
text-align: center;
margin: 1.5rem 0;
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
.separator::before,
.separator::after {
content: '';
flex: 1;
border-bottom: 2px solid var(--border);
}
.separator span {
padding: 0 10px;
background: var(--bg-card);
}
.custom-route-section {
background: rgba(15, 23, 42, 0.4);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
text-align: center;
}
.custom-route-section h3 {
font-size: 1.1rem;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.subtext {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 1rem;
}
.secondary-btn {
width: 100%;
padding: 0.75rem 1.5rem;
background: transparent;
border: 2px solid var(--primary);
color: var(--primary);
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-transform: uppercase;
}
.secondary-btn:hover:not(:disabled) {
background: var(--primary);
color: white;
}
.secondary-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: var(--border);
color: var(--text-secondary);
}
/* KML Browser Styles */
.kml-browser-card {
background: var(--bg-card);
border-radius: 16px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
max-width: 700px;
width: 90%;
max-height: 80vh;
display: flex;
flex-direction: column;
border: 1px solid var(--border);
}
.kml-browser-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 2px solid var(--border);
}
.kml-browser-header h2 {
font-size: 1.5rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 2rem;
cursor: pointer;
transition: all 0.2s ease;
line-height: 1;
}
.close-btn:hover {
color: var(--text-primary);
transform: rotate(90deg);
}
.kml-tabs {
display: flex;
border-bottom: 2px solid var(--border);
padding: 0 2rem;
}
.kml-tab {
padding: 1rem 2rem;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s ease;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.kml-tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.kml-tab:hover {
color: var(--text-primary);
}
.kml-tab-content {
flex: 1;
overflow-y: auto;
padding: 2rem;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
.upload-section {
margin-bottom: 1.5rem;
}
.sort-controls {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
padding: 1rem;
background: rgba(15, 23, 42, 0.4);
border-radius: 8px;
}
.sort-controls label {
font-weight: 500;
color: var(--text-secondary);
}
.sort-controls select {
padding: 0.5rem 1rem;
background: var(--bg-dark);
border: 2px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
font-size: 0.9rem;
cursor: pointer;
}
.sort-controls select:focus {
outline: none;
border-color: var(--primary);
}
.kml-file-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.empty-message {
text-align: center;
color: var(--text-secondary);
padding: 3rem 1rem;
font-style: italic;
}
.kml-file-item {
background: rgba(15, 23, 42, 0.4);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s ease;
}
.kml-file-item:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.1);
}
.kml-file-info {
flex: 1;
}
.kml-file-name {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.kml-file-meta {
display: flex;
gap: 1rem;
font-size: 0.85rem;
color: var(--text-secondary);
}
.kml-file-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.vote-btn {
background: none;
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 0.5rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
.vote-btn:hover {
border-color: var(--primary);
color: var(--text-primary);
}
.vote-btn.active-upvote {
border-color: var(--success);
color: var(--success);
background: rgba(16, 185, 129, 0.1);
}
.vote-btn.active-downvote {
border-color: #ef4444;
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.vote-count {
font-size: 0.9rem;
font-weight: 600;
min-width: 2rem;
text-align: center;
}
.action-btn {
background: none;
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.9rem;
}
.action-btn:hover {
border-color: var(--primary);
color: var(--text-primary);
}
.action-btn.danger:hover {
border-color: #ef4444;
color: #ef4444;
}
/* Secondary Links (Privacy, etc) */
.privacy-link-container {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
width: 100%;
}
.secondary-link {
font-size: 0.85rem;
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease;
}
.secondary-link:hover {
color: var(--primary);
}