Compare commits

...

7 Commits

Author SHA1 Message Date
dan612
8444e1f23a Css map 2026-03-22 17:45:47 -04:00
dan612
5df09b1f2f Updates to pull in analytics. 2026-03-22 17:45:37 -04:00
dan612
b568a283ac Adding new hostname to api webhook controller. 2026-03-22 17:09:31 -04:00
dan612
ffc04634ff Resetting docker compose a bit. 2026-03-22 16:14:13 -04:00
dan
0b90913b5b Merge pull request 'Create dashboard layout' (#2) from issue/1 into main
Reviewed-on: #2
2026-03-22 17:26:20 +00:00
dan612
b9e362497a Adding so many things. 2026-03-22 13:23:31 -04:00
dan612
7d5904e925 Remove todo and update gitignore for mac files. 2026-01-15 10:06:24 -05:00
17 changed files with 268 additions and 92 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
.DS_Store
database/default-content.sql database/default-content.sql
vendor vendor
node_modules node_modules
database/origo.db database/origo.db
scripts/default-content.sh scripts/default-content.sh
docker-compose.override.yml

View File

@@ -1,15 +0,0 @@
FROM php:8.4-fpm
# Install SQLite extension and Node.js
RUN apt-get update && apt-get install -y \
sqlite3 \
libsqlite3-dev \
curl \
&& docker-php-ext-install pdo_sqlite \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
COPY ./src .

37
TODO.md
View File

@@ -1,37 +0,0 @@
# To Do
- [ ] Need to create an agent to send data
- [ ] Need to create api endpoint to handle request
- [ ] Need a new table for accounts
- Users can belong to accounts
## How will authentication work?
JWT or Oauth based tokens
## Lightweight agent
What data will the agent need to send:
1. Gather user data
2. Account id
3. Site id
The agent should be availableas a simple script.
## API Endpoints
```
/api/webhook
```
This will receive a JSON payload from the agent
It will parse the payload to store the data for later display
```
/api/dataset/{account_id}/{site_id}
```
This will provide all metrics to allow displaying
information about the account.:w
```
/api/site-list/{account_id}
```
This will provide a list of sites for a given account.
Each account can have unlimited sites. Each site has an id.

View File

@@ -16,8 +16,6 @@
"guzzlehttp/guzzle": "^7.10" "guzzlehttp/guzzle": "^7.10"
}, },
"scripts": { "scripts": {
"start": "sh scripts/start.sh",
"rebuild": "sh scripts/rebuild.sh",
"theme": "sh scripts/theme.sh", "theme": "sh scripts/theme.sh",
"db-reset": "sh scripts/db-setup.sh" "db-reset": "sh scripts/db-setup.sh"
} }

View File

@@ -55,9 +55,13 @@ CREATE TABLE IF NOT EXISTS user_accounts (
CREATE TABLE IF NOT EXISTS pageviews ( CREATE TABLE IF NOT EXISTS pageviews (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL, account_id TEXT NOT NULL,
page TEXT NOT NULL, page TEXT NOT NULL,
title TEXT,
user_agent TEXT, user_agent TEXT,
screen_res TEXT,
language TEXT,
timestamp TEXT,
referrer TEXT, referrer TEXT,
ip TEXT, ip TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

View File

@@ -1,11 +1,13 @@
services: services:
app: php:
build: . image: dchadwick/planet-express:1.0.1
working_dir: /app
command: php-fpm -F
ports: ports:
- "127.0.0.1:9000:9000" - "127.0.0.1:9000:9000"
volumes: volumes:
- .:/app - .:/app
- ./database:/app/database # Persists your SQLite file - ./database:/app/database
networks: networks:
- app-network - app-network

View File

@@ -25,7 +25,12 @@ if (!$router->routeExists($path)) {
// now we dont have to do this in every controller. // now we dont have to do this in every controller.
// Still allow login on homepage. // Still allow login on homepage.
$request = new Request(); $request = new Request();
$allowed_paths = ['/', '/user-login']; $allowed_paths = [
'/',
'/user-login',
'/api/v1/webhook'
];
if ( if (
!$request->getCookie('ORIGOSESS') && !$request->getCookie('ORIGOSESS') &&
!in_array($path, $allowed_paths) !in_array($path, $allowed_paths)

32
nginx.conf Normal file
View File

@@ -0,0 +1,32 @@
server {
listen 80;
server_name localhost;
# This MUST match the working_dir and volume path from your docker-compose.yml
root /app;
index index.php index.html;
# Using underscores for log files
access_log /var/log/nginx/access_log.log;
error_log /var/log/nginx/error_log.log;
# Route everything to index.php if the file doesn't explicitly exist
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# Pass all .php files to the PHP-FPM container
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# 'php' is the name of the service in your docker-compose.yml
# 9000 is the default port PHP-FPM listens on
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}

View File

@@ -1,2 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
PASSWORD=$1
echo -n $1 | shasum -a 256
openssl rand -hex 16 openssl rand -hex 16

View File

@@ -1,2 +0,0 @@
#!/bin/bash
docker build -t origo .

View File

@@ -1,2 +0,0 @@
#!/bin/bash
docker run --rm -p 8000:8000 -v $(pwd):/app origo php -S 0.0.0.0:8000

View File

@@ -30,30 +30,48 @@ class ApiWebhookController extends ControllerBase implements ControllerInterface
* The response. * The response.
*/ */
public function getResponse(): string { public function getResponse(): string {
header('Access-Control-Allow-Origin: *'); // Allow all origins. // @todo: Create Enum.
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); $allowed_origins = [
header('Access-Control-Allow-Headers: Content-Type, Authorization'); 'http://127.0.0.1:8088',
'http://localhost:8088',
'https://code.danchadwick.dev',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// Check if the requester is in our whitelist
if (in_array($origin, $allowed_origins)) {
header("Access-Control-Allow-Origin: $origin");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
}
// Handle preflight OPTIONS request // Handle preflight OPTIONS request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200); http_response_code(200);
exit(); // Stop here - don't process the request exit(); // Stop here - don't process the request
} }
// Make sure we have a path and account before proceeding.
$path = $this->request->post('path'); $raw_input = file_get_contents('php://input');
$account_id = $this->request->post('account'); $json_data = json_decode($raw_input, true) ?? [];
if ( $path = $json_data['path'] ?? $this->request->post('path');
!$path || $account_id = $json_data['account'] ?? $this->request->post('account');
!$account_id
) { if (!$path || !$account_id) {
// Log what actually arrived to help debugging
error_log("Missing Data - Path: $path, Account: $account_id. Raw: $raw_input");
header("HTTP/1.1 422 Unprocessable Entity"); header("HTTP/1.1 422 Unprocessable Entity");
die(); die("Missing required fields");
} }
// @todo: validate the account id. // @todo: validate the account id.
$entry = [ $entry = [
'page' => $path, 'page' => $path,
'account_id' => $account_id, 'account_id' => $account_id,
'user_agent' => $this->request->post('user_agent'), 'title' => $json_data['title'] ?? null,
'referrer' => $this->request->post('referrer'), 'user_agent' => $json_data['user_agent'] ?? null,
'screen_res' => $json_data['screen_res'] ?? null,
'language' => $json_data['lang'] ?? null,
'timestamp' => $json_data['ts'] ?? null,
'referrer' => $json_data['referrer'] ?? null,
'ip' => $this->request->ip(), 'ip' => $this->request->ip(),
]; ];
$clean_entry = array_filter($entry); $clean_entry = array_filter($entry);

View File

@@ -3,20 +3,59 @@
namespace Origo\Controller; namespace Origo\Controller;
use Origo\Entity\User; use Origo\Entity\User;
use Origo\Controller\ControllerBase;
use Origo\Controller\ControllerInterface; use Origo\Controller\ControllerInterface;
use Origo\Services\Template; use Origo\Services\Template;
use Origo\Services\Request; use Origo\Services\Request;
use Origo\Services\Renderer;
class UserDashboardController implements ControllerInterface { class UserDashboardController extends ControllerBase implements ControllerInterface {
/**
* Get the response for the dashboard.
*/
public function getResponse(): string { public function getResponse(): string {
$user = new User();
$user->loadUserFromSession($this->request->getCookie('ORIGOSESS'));
$user_id = $user->get('id');
$query = $this->database->query("
SELECT pv.page, pv.title, pv.referrer, pv.ip, pv.language, pv.screen_res, pv.timestamp
FROM pageviews pv
JOIN user_accounts ua ON pv.account_id = ua.account_id
WHERE ua.user_id = :user_id
ORDER BY pv.timestamp DESC
");
$query->bindParam(':user_id', $user_id);
$query->execute();
$results = $query->fetchAll();
$rows = '';
if ($results) {
foreach ($results as $row) {
$page = htmlspecialchars($row['page']);
$title = htmlspecialchars($row['title']);
$referrer = htmlspecialchars($row['referrer']);
$ip = htmlspecialchars($row['ip']);
$language = htmlspecialchars($row['language']);
$screen_res = htmlspecialchars($row['screen_res']);
$timestamp = htmlspecialchars($row['timestamp']);
$rows .= "
<tr>
<td>{$title}</td>
<td>{$page}</td>
<td>{$referrer}</td>
<td>{$ip}</td>
<td>{$language}</td>
<td>{$screen_res}</td>
<td>{$timestamp}</td>
</tr>";
}
} else {
$rows = '<tr><td colspan="7">No pageviews yet.</td></tr>';
}
$template = new Template('dashboard.html'); $template = new Template('dashboard.html');
return $template->render([
return $template->render(); 'pageview_rows' => $rows,
]);
} }
} }

View File

@@ -5,9 +5,30 @@
<h4>Actions</h4> <h4>Actions</h4>
<ul id="dash-menu"> <ul id="dash-menu">
<li><a href="/planner" class="btn btn-primary"><span class="material-symbols-outlined">edit_calendar</span>Planner</a></li> <li><a href="/planner" class="btn btn-primary"><span class="material-symbols-outlined">edit_calendar</span>Planner</a></li>
<li><a href="/analytics" class="btn btn-primary"><span class="material-symbols-outlined">area_chart</span>Analytics</a></li>
<li><a href="/user-logout" class="btn btn-accent"><span class="material-symbols-outlined">logout</span>Logout</a></li> <li><a href="/user-logout" class="btn btn-accent"><span class="material-symbols-outlined">logout</span>Logout</a></li>
</ul> </ul>
</div> </div>
<div id="pageviews">
<h2>Pageviews</h2>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th>Title</th>
<th>Page</th>
<th>Referrer</th>
<th>IP</th>
<th>Language</th>
<th>Screen</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{{ pageview_rows }}
</tbody>
</table>
</div>
</div>
</div> </div>
</section> </section>

View File

@@ -1,8 +1,8 @@
#dashboard { #dashboard {
.dashboard-container { .dashboard-container {
max-width: 800px; max-width: 1200px;
margin: auto; margin: auto;
padding-bottom: 100px; padding: 0 24px 100px;
} }
h1.page-title { h1.page-title {
@@ -15,11 +15,71 @@
justify-content: flex-start; justify-content: flex-start;
list-style: none; list-style: none;
padding: 0; padding: 0;
li { li {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
} }
} }
#pageviews {
margin-top: 2rem;
h2 {
margin-bottom: 1rem;
color: var(--color-text-primary);
font-weight: var(--font-weight-bold);
}
.table-wrapper {
overflow-x: auto;
border-radius: 8px;
border: 1px solid var(--color-light-gray);
}
table.data-table {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-small);
white-space: nowrap;
thead tr {
background: var(--color-primary);
color: var(--color-soft-cream);
th {
padding: 0.75rem 1rem;
text-align: left;
font-weight: var(--font-weight-bold);
}
}
tbody {
tr {
border-bottom: 1px solid var(--color-light-gray);
transition: background 0.15s ease;
&:nth-child(even) {
background: var(--color-light-gray);
}
&:nth-child(odd) {
background: var(--color-soft-cream);
}
&:hover {
background: var(--color-surface);
}
td {
padding: 0.65rem 1rem;
color: var(--color-text-primary);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
} }

View File

@@ -482,9 +482,9 @@ img {
} }
#dashboard .dashboard-container { #dashboard .dashboard-container {
max-width: 800px; max-width: 1200px;
margin: auto; margin: auto;
padding-bottom: 100px; padding: 0 24px 100px;
} }
#dashboard h1.page-title { #dashboard h1.page-title {
margin-bottom: 0; margin-bottom: 0;
@@ -501,6 +501,54 @@ img {
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
} }
#dashboard #pageviews {
margin-top: 2rem;
}
#dashboard #pageviews h2 {
margin-bottom: 1rem;
color: var(--color-text-primary);
font-weight: var(--font-weight-bold);
}
#dashboard #pageviews .table-wrapper {
overflow-x: auto;
border-radius: 8px;
border: 1px solid var(--color-light-gray);
}
#dashboard #pageviews table.data-table {
width: 100%;
border-collapse: collapse;
font-size: var(--font-size-small);
white-space: nowrap;
}
#dashboard #pageviews table.data-table thead tr {
background: var(--color-primary);
color: var(--color-soft-cream);
}
#dashboard #pageviews table.data-table thead tr th {
padding: 0.75rem 1rem;
text-align: left;
font-weight: var(--font-weight-bold);
}
#dashboard #pageviews table.data-table tbody tr {
border-bottom: 1px solid var(--color-light-gray);
transition: background 0.15s ease;
}
#dashboard #pageviews table.data-table tbody tr:nth-child(even) {
background: var(--color-light-gray);
}
#dashboard #pageviews table.data-table tbody tr:nth-child(odd) {
background: var(--color-soft-cream);
}
#dashboard #pageviews table.data-table tbody tr:hover {
background: var(--color-surface);
}
#dashboard #pageviews table.data-table tbody tr td {
padding: 0.65rem 1rem;
color: var(--color-text-primary);
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
#task-list .container { #task-list .container {
max-width: 800px; max-width: 800px;

View File

@@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["partials/reset.scss","partials/base.scss","partials/header.scss","partials/animations.scss","partials/login-form.scss","partials/403.scss","partials/dashboard.scss","partials/task-list.scss","style.scss"],"names":[],"mappings":"AAAA;AACA;EACE;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;IACE;;;AAIJ;AACE;EACA;AACA;EACA;;;AAGF;AACA;EACE;EACA;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;;;AAEF;EACE;;;AAGF;AAAA;AAAA;AAGA;EACE;;;ACpDF;AACE;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EACE;;AAIJ;EACE;;AAEA;EACE;;AAIJ;EACE;;AAEA;EACE;;;AC9EN;EACE;EACA;EACA;;;AAIA;EACE;EACA;EACA;;;AAIJ;EACE;;;ACfF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;;ACnBF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;EACA;;AAGF;EACE;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;;;AC9IN;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;IAAW;;EACX;IAAM;;;AAGR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;IAAW;;EACX;IAAM;;EACN;IAAM;;;AAGR;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;IAAK;;;AAGP;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;;;AC5MJ;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;;ACpBJ;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;ACrDF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA","file":"style.css"} {"version":3,"sourceRoot":"","sources":["partials/reset.scss","partials/base.scss","partials/header.scss","partials/animations.scss","partials/login-form.scss","partials/403.scss","partials/dashboard.scss","partials/task-list.scss","style.scss"],"names":[],"mappings":"AAAA;AACA;EACE;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;IACE;;;AAIJ;AACE;EACA;AACA;EACA;;;AAGF;AACA;EACE;EACA;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;;;AAEF;EACE;;;AAGF;AAAA;AAAA;AAGA;EACE;;;ACpDF;AACE;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;AAEA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAEA;EACA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EACE;;AAIJ;EACE;;AAEA;EACE;;AAIJ;EACE;;AAEA;EACE;;;AC9EN;EACE;EACA;EACA;;;AAIA;EACE;EACA;EACA;;;AAIJ;EACE;;;ACfF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;;ACnBF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;AAAA;EAEE;EACA;EACA;EACA;;AAGF;EACE;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;;;AC9IN;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;IAAW;;EACX;IAAM;;;AAGR;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;IAAW;;EACX;IAAM;;EACN;IAAM;;;AAGR;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;IAAK;;;AAGP;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;IACE;;EAGF;IACE;;EAGF;IACE;;EAGF;IACE;IACA;;EAGF;IACE;;;;AC5MJ;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAKF;EACE;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AC7EV;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;;;ACrDF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA","file":"style.css"}