<?php
if (!defined('HOBBES')) { http_response_code(403); exit; }
$_settings = settings_load();
$_css = $_settings['css'];
$_role = effective_role();
$_user = current_user();
// Apply user's personal theme if set, overriding the site default
if ($_user && !empty($_user['theme_preset']) && isset(THEME_PRESETS[$_user['theme_preset']])) {
$_css = THEME_PRESETS[$_user['theme_preset']]['css'];
}
$_flashes = get_flashes();
$_site = h($_settings['site_name']);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title><?php echo isset($page_title) ? h($page_title) . ' - ' . $_site : $_site; ?></title>
<script>(function(){var s=[13,15,17,20],i=+(localStorage.getItem('hfsz')||1);i=Math.max(0,Math.min(3,i));document.documentElement.style.fontSize=s[i]+'px';window.hFontScale=function(d){i=Math.max(0,Math.min(3,i+d));document.documentElement.style.fontSize=s[i]+'px';try{localStorage.setItem('hfsz',i);}catch(e){}};})();</script>
<style>
/* ── CSS Custom Properties ──────────────────── */
:root {
--c-bg: <?php echo h($_css['bg_color']); ?>;
--c-text: <?php echo h($_css['text_color']); ?>;
--c-link: <?php echo h($_css['link_color']); ?>;
--c-visited: <?php echo h($_css['visited_color']); ?>;
--c-panel-bg: <?php echo h($_css['panel_bg']); ?>;
--c-btn-bg: <?php echo h($_css['btn_bg']); ?>;
--c-btn-text: <?php echo h($_css['btn_text']); ?>;
--c-border: <?php echo h($_css['border_color']); ?>;
--c-accent: <?php echo h($_css['accent']); ?>;
--c-th-bg: <?php echo h($_css['th_bg']); ?>;
--c-th-text: <?php echo h($_css['th_text']); ?>;
--c-tr-alt: <?php echo h($_css['tr_alt_bg']); ?>;
--c-tr-hover: <?php echo h($_css['tr_hover_bg'] ?? $_css['tr_alt_bg']); ?>;
--c-muted: <?php echo h($_css['muted_color'] ?? '#666666'); ?>;
--c-nav-bg: <?php echo h($_css['nav_bg']); ?>;
--c-nav-text: <?php echo h($_css['nav_text']); ?>;
--c-nav-hover: <?php echo h($_css['nav_hover_bg']); ?>;
--c-hdr-bg: <?php echo h($_css['header_bg']); ?>;
--c-hdr-text: <?php echo h($_css['header_text']); ?>;
--c-input-bg: <?php echo h($_css['input_bg']); ?>;
--c-input-txt: <?php echo h($_css['input_text'] ?? $_css['text_color']); ?>;
--c-input-bdr: <?php echo h($_css['input_border']); ?>;
--c-err-bg: <?php echo h($_css['error_bg']); ?>;
--c-ok-bg: <?php echo h($_css['success_bg']); ?>;
--c-info-bg: <?php echo h($_css['info_bg']); ?>;
}
/* ── Reset ─────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: 15px; }
body {
font-family: 'Courier New', Courier, monospace;
font-size: 1rem;
background: var(--c-bg);
color: var(--c-text);
min-height: 100vh;
}
/* ── Links as buttons ───────────────────────── */
a {
color: var(--c-btn-text);
background: var(--c-btn-bg);
border: 1px solid var(--c-border);
padding: 0 4px;
text-decoration: none;
}
a:visited { color: var(--c-visited); background: var(--c-btn-bg); }
a:hover {
background: var(--c-btn-text);
color: var(--c-btn-bg);
text-decoration: none;
}
/* ── Layout ────────────────────────────────── */
#wrap { max-width: 1000px; margin: 0 auto; }
/* ── Header ────────────────────────────────── */
#site-header {
background: var(--c-hdr-bg);
color: var(--c-hdr-text);
padding: 6px 10px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 3px ridge var(--c-border);
}
#site-header a {
color: var(--c-hdr-text);
background: transparent;
border: none;
padding: 0 4px;
text-decoration: none;
}
#site-header a:visited { color: var(--c-hdr-text); background: transparent; }
#site-header a:hover { background: rgba(255,255,255,0.15); color: var(--c-hdr-text); }
#site-header h1 { font-size: 1.2rem; font-weight: bold; letter-spacing: 1px; }
#site-header .tagline { font-size: 0.75rem; opacity: 0.85; }
#user-bar { font-size: 0.75rem; text-align: right; }
#user-bar a { color: var(--c-hdr-text); margin-left: 8px; background: transparent; border: none; }
#user-bar a:hover { background: rgba(255,255,255,0.15); color: var(--c-hdr-text); }
/* ── Navigation ────────────────────────────── */
#nav {
background: var(--c-nav-bg);
padding: 0;
border-bottom: 2px solid var(--c-border);
display: flex;
flex-wrap: wrap;
}
#nav a {
color: var(--c-nav-text);
background: transparent;
border: none;
border-right: 1px solid rgba(255,255,255,0.2);
text-decoration: none;
padding: 5px 12px;
display: inline-block;
font-weight: bold;
font-size: 0.8rem;
}
#nav a:visited { color: var(--c-nav-text); background: transparent; }
#nav a:hover { background: var(--c-nav-hover); color: var(--c-nav-text); }
#nav a.active { background: var(--c-nav-hover); color: var(--c-nav-text); border-bottom: 2px solid #fff; }
/* ── Content ───────────────────────────────── */
#content { display: flex; gap: 0; }
#sidebar {
width: 180px;
min-width: 180px;
background: var(--c-panel-bg);
border-right: 1px solid var(--c-border);
padding: 8px;
}
#main { flex: 1; padding: 10px; min-width: 0; }
/* ── Panels / boxes ────────────────────────── */
.panel {
background: var(--c-panel-bg);
border: 2px solid var(--c-border);
margin-bottom: 10px;
}
.panel-title {
background: var(--c-th-bg);
color: var(--c-th-text);
padding: 3px 8px;
font-weight: bold;
font-size: 0.8rem;
}
.panel-body { padding: 8px; }
/* ── Tables ────────────────────────────────── */
table.listing { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
table.listing th {
background: var(--c-th-bg);
color: var(--c-th-text);
padding: 3px 6px;
text-align: left;
border: 1px solid var(--c-border);
white-space: nowrap;
}
table.listing td {
padding: 2px 6px;
border: 1px solid var(--c-border);
vertical-align: top;
color: var(--c-text);
background: var(--c-bg);
}
table.listing tr:nth-child(even) td { background: var(--c-tr-alt); }
table.listing tr:hover td { background: var(--c-tr-hover); }
table.listing .icon { font-family: monospace; font-size: 0.75rem; white-space: nowrap; }
table.listing .size { text-align: right; white-space: nowrap; }
table.listing .date { white-space: nowrap; }
/* ── Forms ─────────────────────────────────── */
/* Global input theming — applies to all inputs, including sidebar search */
input, select, textarea {
background: var(--c-input-bg);
color: var(--c-input-txt);
border: 1px solid var(--c-input-bdr);
}
form.std label { display: block; margin-top: 8px; font-weight: bold; font-size: 0.8rem; }
form.std input[type=text],
form.std input[type=password],
form.std input[type=email],
form.std input[type=url],
form.std input[type=number],
form.std select,
form.std textarea {
width: 100%;
background: var(--c-input-bg);
color: var(--c-input-txt);
border: 2px inset var(--c-input-bdr);
padding: 3px 5px;
font-family: inherit;
font-size: 0.8rem;
margin-top: 2px;
}
form.std textarea { height: 200px; resize: vertical; }
form.std .hint { font-size: 0.75rem; color: var(--c-muted); margin-top: 2px; }
form.std .row { margin-top: 12px; }
/* ── Buttons ───────────────────────────────── */
.btn {
display: inline-block;
background: var(--c-btn-bg);
color: var(--c-btn-text);
border: 2px outset var(--c-border);
padding: 3px 12px;
font-family: inherit;
font-size: 0.8rem;
cursor: pointer;
text-decoration: none;
}
.btn:visited { background: var(--c-btn-bg); color: var(--c-btn-text); }
.btn:hover { border-style: inset; background: var(--c-btn-text); color: var(--c-btn-bg); }
.btn-primary {
background: var(--c-th-bg);
color: var(--c-th-text);
border-color: var(--c-accent);
}
.btn-primary:visited { background: var(--c-th-bg); color: var(--c-th-text); }
.btn-primary:hover { background: var(--c-th-text); color: var(--c-th-bg); border-style: inset; }
.btn-danger { background: #cc0000; color: #fff; border-color: #880000; }
.btn-danger:visited { background: #cc0000; color: #fff; }
.btn-danger:hover { background: #fff; color: #cc0000; border-style: inset; }
.btn-small { padding: 1px 6px; font-size: 0.75rem; }
/* ── Flash messages ────────────────────────── */
.flash { padding: 5px 10px; margin-bottom: 8px; border: 1px solid; font-size: 0.8rem; color: var(--c-text); }
.flash-error { background: var(--c-err-bg); border-color: #cc0000; }
.flash-success { background: var(--c-ok-bg); border-color: #009900; }
.flash-info { background: var(--c-info-bg); border-color: var(--c-accent); }
/* ── Category tree ─────────────────────────── */
.cat-tree, .cat-tree ul { list-style: none; padding-left: 10px; margin: 0; }
.cat-tree li { padding: 1px 0; }
.cat-tree li a {
background: transparent;
border: none;
color: var(--c-link);
font-size: 0.8rem;
text-decoration: none;
padding: 0;
}
.cat-tree li a:visited { background: transparent; color: var(--c-visited); }
.cat-tree li a:hover { background: transparent; color: var(--c-link); text-decoration: underline; }
.cat-tree li.active > a { font-weight: bold; }
.cat-tree > li { padding-left: 0; }
/* details/summary for collapsible subcategories */
.cat-tree details { display: block; }
.cat-tree summary.cat-summary {
cursor: pointer;
font-size: 0.8rem;
list-style: none;
padding: 0;
display: block;
}
.cat-tree summary.cat-summary::-webkit-details-marker { display: none; }
.cat-tree summary.cat-summary::marker { display: none; }
.cat-tree summary.cat-summary::before {
content: '[+] ';
font-family: monospace;
font-size: 0.65rem;
color: var(--c-muted);
}
.cat-tree details[open] > summary.cat-summary::before { content: '[-] '; }
/* ── Pagination ────────────────────────────── */
.pagination { margin: 10px 0; }
.pagination a {
display: inline-block;
padding: 2px 6px;
border: 1px solid var(--c-border);
margin: 1px;
text-decoration: none;
background: var(--c-btn-bg);
color: var(--c-btn-text);
}
.pagination a:visited { background: var(--c-btn-bg); color: var(--c-btn-text); }
.pagination a:hover { background: var(--c-btn-text); color: var(--c-btn-bg); }
.pagination a.current {
background: var(--c-accent);
color: var(--c-th-text);
font-weight: bold;
border-color: var(--c-accent);
}
/* ── Markdown content ──────────────────────── */
.md-content h1, .md-content h2, .md-content h3 {
margin: 12px 0 6px;
color: var(--c-accent);
}
.md-content p { margin: 6px 0; line-height: 1.5; }
.md-content ul, .md-content ol { margin: 6px 0 6px 20px; }
.md-content code { background: var(--c-tr-alt); color: var(--c-text); padding: 0 3px; font-size: 0.75rem; }
.md-content pre { background: var(--c-th-bg); color: var(--c-th-text); padding: 8px; overflow-x: auto; margin: 8px 0; }
.md-content blockquote { border-left: 3px solid var(--c-accent); padding-left: 8px; color: var(--c-muted); }
.md-content hr { border: 0; border-top: 1px solid var(--c-border); margin: 10px 0; }
/* ── Admin ─────────────────────────────────── */
.admin-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.admin-card {
background: var(--c-panel-bg);
border: 2px solid var(--c-border);
padding: 10px;
width: 180px;
text-align: center;
}
.admin-card a { font-weight: bold; }
.color-swatch { display: inline-block; width: 20px; height: 14px; border: 1px solid var(--c-border); vertical-align: middle; }
/* ── Misc ──────────────────────────────────── */
.info-box { border: 1px solid var(--c-border); background: var(--c-panel-bg); padding: 8px; margin: 8px 0; }
.tag { display: inline-block; background: var(--c-th-bg); color: var(--c-th-text); padding: 1px 5px; font-size: 0.75rem; margin: 1px; border: none; }
.tag:visited { background: var(--c-th-bg); color: var(--c-th-text); }
.tag:hover { background: var(--c-th-text); color: var(--c-th-bg); }
.role-badge { font-size: 0.65rem; font-weight: bold; padding: 1px 4px; border: 1px solid; }
.role-admin { background: #400; color: #faa; border-color: #800; }
.role-editor { background: #040; color: #afa; border-color: #080; }
.role-contributor { background: #004; color: #aaf; border-color: #008; }
.role-guest { background: #444; color: #ddd; border-color: #666; }
/* Status indicator colors that adapt to theme */
.status-ok { color: var(--c-accent); }
.status-warn { color: var(--c-link); }
.status-err { color: #ff6666; }
hr { border: 0; border-top: 1px solid var(--c-border); margin: 8px 0; }
.right { float: right; }
.clear { clear: both; }
.muted { color: var(--c-muted); font-size: 0.75rem; }
<?php if (!empty($_settings['two_spot']['enabled'])):
$_ts_bg = h($_settings['two_spot']['bg'] ?? '#000080');
$_ts_text = h($_settings['two_spot']['text'] ?? '#ffff00');
?>
/* ── IBM Two Spot ───────────────────────── */
:root { --ts-bg: <?php echo $_ts_bg; ?>; --ts-fg: <?php echo $_ts_text; ?>; }
*, *::before, *::after {
background-color: var(--ts-bg) !important;
color: var(--ts-fg) !important;
border-color: var(--ts-fg) !important;
}
a, a:visited, .btn, .btn:visited, .btn-primary, .btn-danger {
background-color: var(--ts-bg) !important;
color: var(--ts-fg) !important;
border-color: var(--ts-fg) !important;
}
a:hover, .btn:hover, .btn-primary:hover, .btn-danger:hover,
#nav a:hover, #nav a.active,
table.listing tr:hover td,
.cat-tree li a:hover,
.pagination a.current, .pagination a:hover {
background-color: var(--ts-fg) !important;
color: var(--ts-bg) !important;
}
<?php endif; ?>
</style>
</head>
<body>
<div id="wrap">
<div id="site-header">
<div>
<a href="/"><h1><?php echo $_site; ?></h1></a>
<div class="tagline"><?php echo h($_settings['site_tagline']); ?></div>
</div>
<div id="user-bar">
<?php if ($_user): ?>
Logged in as <strong><?php echo h($_user['username']); ?></strong>
<span class="role-badge role-<?php echo h($_user['role']); ?>"><?php echo strtoupper(h($_user['role'])); ?></span>
<a href="/profile">Profile</a>
<?php if (can('invite')): ?><a href="/invite">Invites</a><?php endif; ?>
<?php if (can('admin')): ?><a href="/admin">Admin</a><?php endif; ?>
<a href="/logout">Logout</a>
<?php elseif ($_role === 'guest'): ?>
<span class="role-badge role-guest">OS/2 GUEST</span>
<a href="/login">Login</a>
<?php else: ?>
<a href="/login">Login</a>
<?php $s = settings_load(); if (!empty($s['open_registration'])): ?>
<a href="/register">Register</a>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<div id="nav">
<a href="/"<?php echo ($_page ?? '') === 'home' ? ' class="active"' : ''; ?>>Home</a>
<?php if (can('browse')): ?>
<a href="/browse"<?php echo ($_page ?? '') === 'browse' ? ' class="active"' : ''; ?>>Browse</a>
<a href="/search"<?php echo ($_page ?? '') === 'search' ? ' class="active"' : ''; ?>>Search</a>
<?php endif; ?>
<?php if (can('upload')): ?>
<a href="/upload"<?php echo ($_page ?? '') === 'upload' ? ' class="active"' : ''; ?>>Upload</a>
<?php endif; ?>
<?php if (can('approve')): ?>
<a href="/pool"<?php echo ($_page ?? '') === 'pool' ? ' class="active"' : ''; ?>>Pool</a>
<?php endif; ?>
<?php if (can('admin')): ?>
<a href="/admin"<?php echo ($_page ?? '') === 'admin' ? ' class="active"' : ''; ?>>Admin</a>
<?php endif; ?>
<span style="margin-left:auto;display:flex;align-items:center;">
<a href="#" onclick="if(window.hFontScale)hFontScale(-1);return false;" style="padding:5px 8px;border-right:0;" title="Decrease font size">A-</a>
<a href="#" onclick="if(window.hFontScale)hFontScale(1);return false;" style="padding:5px 8px;" title="Increase font size">A+</a>
</span>
</div>
<?php if ($_flashes): ?>
<div id="flashes">
<?php foreach ($_flashes as $f): ?>
<div class="flash flash-<?php echo h($f['type']); ?>"><?php echo h($f['msg']); ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div id="content">
<div id="sidebar">
<?php if (can('browse')): ?>
<div class="panel">
<div class="panel-title">Categories</div>
<div class="panel-body">
<?php
$_cats = categories_load();
$_tree = build_category_tree($_cats);
echo render_category_tree($_tree, $active_category ?? '');
if (empty($_cats)) echo '<p class="muted">No categories yet.</p>';
?>
</div>
</div>
<div class="panel">
<div class="panel-title">Search</div>
<div class="panel-body">
<form method="get" action="/search">
<input type="text" name="q" value="<?php echo h($_GET['q'] ?? ''); ?>" style="width:100%;">
<button type="submit" class="btn" style="margin-top:4px;width:100%">Go</button>
</form>
</div>
</div>
<?php else: ?>
<div class="panel">
<div class="panel-title">Access</div>
<div class="panel-body">
<p class="muted" style="line-height:1.5;">
<a href="/login">Login</a> or
<a href="/register/invite">use an invite</a>
to browse the archive.
</p>
</div>
</div>
<?php endif; ?>
</div>
<div id="main">