/*
  Vortex BBS - modernized stylesheet.
  Same palette and "soul" as the production style.css (black background, green/magenta
  terminal accents), rebuilt with CSS custom properties, grid, and a real mobile
  breakpoint instead of a fixed 980px float layout.
*/

/* Perfect DOS VGA 437 - the actual IBM PC VGA text-mode font, already sitting in
   fonts/8x16.* unused. Self-hosted, no external request.
   unicode-range excludes U+00A0 (non-breaking space) on purpose: this font's glyph
   table maps that codepoint to the CP437 character 'á', which is literally what
   occupied that memory slot in real DOS - authentic, but it means every &nbsp; in the
   BBS-data modules (lastcalls.php etc. use it for cell padding) rendered as a visible
   letter instead of a blank space. Excluding it here makes the browser fall through to
   the next font in the stack for just that one character, which renders it correctly. */
@font-face {
    font-family: 'Perfect DOS VGA 437';
    src: url('../fonts/8x16.woff') format('woff'),
         url('../fonts/8x16.ttf') format('truetype');
    font-display: swap;
    unicode-range: U+0000-009F, U+00A1-FFFF;
}

:root {
    --bg: #000000;
    --fg: #ffffff;
    --fg-muted: #999999;
    --fg-dim: #555555;
    --accent-green: #55ff55;
    --accent-magenta: #950095;
    --accent-red: #f02025;
    --border: #333333;
    --nav-bg: #222222;
    --panel-bg: #000000;
    --font-mono: 'Perfect DOS VGA 437', 'Ubuntu Mono', 'DejaVu Sans Mono', 'Courier New', monospace;
    --content-max: 980px;
}

*, *::before, *::after {
    box-sizing: border-box;
}

html, body {
    margin: 0;
    padding: 0;
}

body {
    background: var(--bg);
    color: var(--fg);
    font-family: var(--font-mono);
    font-size: 16px;
    line-height: 1.3;
    /* Slight phosphor bleed on every glyph, inherited site-wide - much smaller radius
       than the h1/h2/nav glow below so it reads as ambient CRT softness rather than a
       glow effect in its own right. */
    text-shadow: 0 0 1px currentColor;
}

a {
    color: var(--accent-magenta);
    font-weight: bold;
    text-decoration: none;
}

a:hover, a:focus {
    color: var(--accent-green);
}

img {
    max-width: 100%;
    height: auto;
}

h1, h2 {
    font-weight: normal;
    letter-spacing: -0.02em;
    color: var(--accent-green);
    margin: 0 0 0.4em 0;
}

h1 {
    font-size: 2em;
}

h2 {
    font-size: 1.1em;
}

p {
    margin: 0 0 0.75em 0;
}

/* ---- Header & nav ---- */

.site-header {
    text-align: center;
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
}

.site-header img {
    /* Native resolution is 1524x229 - deliberately upscaled here to fill more of the
       header the same way the other pixel-art banners do, with image-rendering keeping
       it crisp rather than blurry at the larger size. */
    max-width: 1180px;
    width: 100%;
    height: auto;
    image-rendering: pixelated;
}

.site-nav {
    background: var(--nav-bg);
    border-bottom: 1px solid var(--border);
}

.site-nav ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    max-width: var(--content-max);
    margin: 0 auto;
}

.site-nav a {
    display: block;
    padding: 1em 1.4em;
    font-size: 1em;
    font-weight: normal;
    text-transform: uppercase;
    color: var(--fg-muted);
    border-right: 1px solid var(--border);
}

.site-nav a:hover,
.site-nav a[aria-current="page"] {
    background: var(--bg);
    color: var(--accent-green);
}

/* Mobile nav: the flex row above just wraps with no handling below a certain width -
   six links at 1em/1.4em padding each don't fit one line on a phone, so it broke into
   ragged multi-row wrapping. Below 780px, hide the link list behind a toggle button
   instead. Hamburger icon is drawn with CSS (border/pseudo-elements), not a Unicode
   character - avoids the same DOS-font glyph-table collision found earlier with
   &nbsp;/&mdash; (this font doesn't reliably cover arbitrary Unicode symbols). */
.nav-toggle {
    display: none;
    align-items: center;
    gap: 0.6em;
    width: 100%;
    background: transparent;
    border: none;
    color: var(--fg);
    font-family: var(--font-mono);
    font-size: 1em;
    text-transform: uppercase;
    padding: 0.8em 1em;
    cursor: pointer;
}

.nav-toggle:hover,
.site-nav.is-open .nav-toggle {
    color: var(--accent-green);
}

.nav-toggle-icon {
    position: relative;
    display: block;
    width: 20px;
    height: 2px;
    background: currentColor;
}

.nav-toggle-icon::before,
.nav-toggle-icon::after {
    content: '';
    position: absolute;
    left: 0;
    width: 20px;
    height: 2px;
    background: currentColor;
}

.nav-toggle-icon::before {
    top: -6px;
}

.nav-toggle-icon::after {
    top: 6px;
}

@media (max-width: 780px) {
    .nav-toggle {
        display: flex;
    }

    .site-nav ul {
        display: none;
        flex-direction: column;
        max-width: none;
    }

    .site-nav.is-open ul {
        display: flex;
    }

    .site-nav a {
        border-right: none;
        border-top: 1px solid var(--border);
    }
}

/* ---- Page layout ----
   Three purposeful columns, matching the original site's intent:
     live  = the BBS's live pulse (last callers, who's online, stats, oneliners...)
     blog  = the "what's new" sysop blog - the actual primary content
     ads   = affiliate BBS banners / related community & software ads
   On mobile these stack with the blog (content) first via grid-template-areas,
   without needing to reorder the markup itself. */

.layout {
    max-width: 1180px;
    margin: 0 auto;
    padding: 12px 16px;
    display: grid;
    gap: 12px;
    grid-template-areas:
        "blog"
        "live"
        "ads";
    grid-template-columns: 1fr;
}

/* call.php and chat.php: no sidebar at all in production (just the ftelnet/IRC embed
   filling the whole content area), so no ads column here either. Both add .constrained
   for the same 1580px cap as the content pages - fTelnet's own terminal grid doesn't
   stretch to fill available width regardless (it renders at its own intrinsic size), but
   the surrounding page still reads more consistent with the rest of the site centered
   and bounded at the same width, rather than one page being the odd one out at full
   viewport width. .layout.full-width itself (no .constrained) is available uncapped if a
   future page genuinely needs it. */
.layout.full-width {
    grid-template-areas: "blog";
    max-width: none;
}

.layout.full-width.constrained {
    max-width: 1580px;
}

/* Every page except the homepage has no .area-live element at all - the base template
   above names a "live" row that would just go unused, but overriding it explicitly
   avoids relying on that being safe rather than just happening to look fine. */
.layout.two-col {
    grid-template-areas:
        "blog"
        "ads";
}

@media (min-width: 900px) {
    .layout {
        max-width: 1580px;
        /* live: grown further (385px->460px) - the lastcalls table (caller/stays-at/
           points/last) was tight enough that table-layout:auto squeezed cell padding
           toward zero on the widest rows ("equilibrium" touching "new york" with no gap
           at all). ads grown proportionally (265px->316px, same ~19% factor) rather than
           just left behind, and the page max-width grown by more than that so blog gets
           real extra room too, not just whatever live/ads leave over. Gaps/padding still
           intentionally untouched so the spacing rhythm itself doesn't grow. */
        grid-template-areas: "live blog ads";
        grid-template-columns: 460px minmax(0, 1fr) 316px;
        align-items: start;
    }

    /* Every page except the homepage: no live-widgets column, just content + ads
       sidebar - matches production, where the three-part split (live/blog/ads) only
       ever existed on index.php; everything else was a plain two-column #content +
       #subcontent layout. */
    .layout.two-col {
        grid-template-areas: "blog ads";
        grid-template-columns: minmax(0, 1fr) 316px;
    }

    /* Bug: .layout.full-width (below, outside this media query) only overrode
       grid-template-areas, not grid-template-columns - so it kept inheriting the base
       .layout rule's three explicit column tracks (460px minmax(0,1fr) 316px) from right
       here. Since the "blog" area name only appears once in that override's area string,
       it only ever occupied the first (460px) column track - which is exactly the fixed
       width the chat/call embeds were stuck at regardless of viewport width. */
    .layout.full-width {
        grid-template-columns: 1fr;
    }
}

/* Grid items default to a min-width based on their content's natural size - without
   this, a table with fixed-width cells (e.g. lastcalls' 50+250+20+40=360px) refuses to
   shrink to fit its track and instead overflows into the next column. This is what
   caused the garbled/overlapping text between the live and blog columns. */
.layout > * {
    min-width: 0;
}

.area-live {
    grid-area: live;
    display: grid;
    gap: 10px;
    min-width: 0;
}

.area-blog {
    grid-area: blog;
    min-width: 0;
}

.area-ads {
    grid-area: ads;
    display: grid;
    gap: 10px;
    min-width: 0;
}

/* Only .area-live goes 2-column at tablet widths - its content is all wrapped in our
   own .panel-item groups, so a box always stays paired with its caption. .area-ads
   partly echoes raw content straight from dynamic/ads.dat and boards.dat, where each
   box and its caption are separate sibling elements rather than one grouped unit - a
   2-column grid could split a box from its own caption into different columns. Keeping
   it single-column avoids that regardless of viewport width. */
@media (min-width: 480px) and (max-width: 899px) {
    .area-live {
        grid-template-columns: 1fr 1fr;
    }
}

/* ---- Blog (What's New) ---- */

.blog-intro {
    border-bottom: 1px solid var(--border);
    padding-bottom: 10px;
    margin-bottom: 12px;
}

/* This intro line sits right under the glowing h1, so it gets a stronger phosphor
   glow than ordinary body text to match rather than looking abruptly crisp next to it.
   Not applied to paragraph text generally (post bodies, etc.) - that stays at the
   site-wide subtle 1px so long-form reading text doesn't lose sharpness. */
.blog-intro p {
    text-shadow: 0 0 3px currentColor;
}

.blog h2.section-title {
    text-align: left;
    color: var(--fg);
    font-size: 1.1em;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    border-bottom: 1px solid var(--border);
    padding-bottom: 6px;
    margin-bottom: 10px;
}

.post {
    padding-bottom: 12px;
    margin-bottom: 12px;
    border-bottom: 1px solid var(--border);
    line-height: 1.35;
}

/* scores.php's user leaderboard. Production re-declared this same table styling inline
   on the page itself; same look (magenta header, zebra rows, green hover) via the
   shared stylesheet instead. */
.data-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 16px;
}

.data-table th {
    background: var(--accent-magenta);
    color: #ffffff;
    text-align: left;
    padding: 8px 10px;
    font-weight: normal;
}

.data-table td {
    padding: 6px 10px;
}

.data-table tr:nth-child(even) {
    background: #060606;
}

.data-table tr:hover td {
    background: #060606;
    color: var(--accent-green);
}

.trend-down {
    color: #ff3b3b;
}

.trend-up {
    color: var(--accent-green);
}

.chat-frame {
    display: block;
    width: 100%;
    height: 70vh;
    min-height: 450px;
    border: 1px solid var(--border);
}

/* call.php's fTelnet terminal. Unlike the chat iframe above, this doesn't respond to
   CSS width - it draws the actual 80-column terminal onto a <canvas> at a fixed native
   pixel size (960px wide at its default font, no exposed zoom/font-size option), so
   width:100% has nothing to act on. Scaling it up visually with transform is a real
   tradeoff (the bitmap is still 960px natively, so it gets blockier at a larger size) -
   done here at the user's explicit choice, not assumed.
   960px * 1.6 = 1536px, fitting cleanly within the ~1548px available width.
   transform doesn't affect the space reserved for layout, so without the margin-bottom
   below the scaled-up terminal would visually overlap whatever follows it on the page.
   #fTelnetContainer auto-sizes to its children's height, so growing the child's layout
   footprint via margin is enough - IF that margin actually applies: ftelnet.css's own
   `#fTelnetClientContainer { margin: auto; }` loads after this stylesheet and, at equal
   specificity, would otherwise win and silently zero out margin-bottom. The extra
   #fTelnetContainer ancestor in this selector (two ID selectors instead of one) beats it
   without needing !important. */
#fTelnetContainer #fTelnetClientContainer {
    transform: scale(1.6);
    transform-origin: top center;
    margin-bottom: 345px;
}

/* contact.php's form. */
.vortex-form {
    max-width: 500px;
    margin: 0 0 20px 0;
}

.vortex-form label {
    display: block;
    margin: 0 0 4px 0;
    font-weight: bold;
    color: var(--accent-green);
}

.vortex-form input[type="text"],
.vortex-form input[type="email"],
.vortex-form textarea {
    width: 100%;
    padding: 8px;
    margin: 0 0 16px 0;
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--fg);
    font-family: var(--font-mono);
    font-size: 1em;
}

.vortex-form input[type="text"]:focus,
.vortex-form input[type="email"]:focus,
.vortex-form textarea:focus {
    outline: none;
    border-color: var(--accent-green);
}

.vortex-form textarea {
    resize: vertical;
}

.vortex-form button {
    padding: 10px 24px;
    background: var(--bg);
    border: 1px solid var(--accent-green);
    color: var(--accent-green);
    font-family: var(--font-mono);
    font-size: 1em;
    font-weight: bold;
    cursor: pointer;
}

.vortex-form button:hover {
    background: var(--accent-green);
    color: var(--bg);
}

.hp-field {
    position: absolute !important;
    left: -9999px !important;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.form-success {
    max-width: 500px;
    padding: 12px;
    margin: 0 0 20px 0;
    border: 1px solid var(--accent-green);
    color: var(--accent-green);
    background: var(--bg);
}

.form-errors {
    max-width: 500px;
    padding: 12px;
    margin: 0 0 20px 0;
    border: 1px solid var(--accent-red);
    color: var(--accent-red);
    background: var(--bg);
}

.form-errors ul {
    margin: 0 0 0 18px;
}

.post-date {
    font-weight: bold;
    letter-spacing: 0.03em;
    margin: 0 0 4px 0;
}

.post-body {
    /* Was capped at 74ch to keep a comfortable reading measure, but that left a visible
       ~82px dead gap on the right at the blog column's current (wider) width - now fills
       the full column instead. */
    width: 100%;
    margin: 0;
    /* Greedy wrap at a fixed width leaves frequent single-word orphan lines with
       monospace text ("Primarily", "Some", "our IRC" alone on their own line) -
       text-wrap: pretty specifically avoids that (Chromium/Firefox support; harmless
       no-op fallback to normal wrap elsewhere). That alone wasn't enough at narrower
       viewports though: a long word ("deliberating", "difficulty") that doesn't fit at
       the end of a line gets pushed whole to the next line, often leaving just that one
       word before the next long word does the same thing. hyphens lets the browser
       break the word with a hyphen instead, which is what actually fixes that pattern. */
    text-wrap: pretty;
    hyphens: auto;
}

.post:last-child {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
}

/* ---- Panels (the boxed widgets: last calls, who's online, stats, ads, etc.) ----
   Production's convention: a box (image header + content), with its caption as a
   separate small label directly underneath - not inside the box. `.panel-item` groups
   each pair so the grid's gap lands between different widgets, not between a widget and
   its own caption. `.box`/`.small` are the exact class names production's own data files
   (dynamic/ads.dat, dynamic/boards.dat) already embed in their raw HTML - defining them
   here means that content renders correctly without any template changes on our end. */

.panel, .box {
    display: block;
    border: 1px solid var(--border);
    background: var(--panel-bg);
    color: var(--fg-muted);
    padding: 8px;
    text-decoration: none;
}

a.panel, .box a {
    color: var(--fg-muted);
}

/* Was a.panel:hover only, so it worked on the left column (all four widgets are real
   links to call.php) but not the right column (mostly plain divs - only some contain a
   link inside, like Plan 9's "contact" or the affiliate-board images). Broadened to
   every panel so hover feedback is consistent regardless of whether the whole box
   happens to be a link. */
.panel:hover, .box:hover {
    border-color: var(--accent-green);
}

/* Same mismatch as .panel-caption below: .small comes from production's original class
   names embedded in ads.dat/boards.dat and shrank that box text to 0.85em (13.6px) while
   our own hand-authored box text (e.g. the Plan 9 blurb) sits at the full 1em (16px).
   Matched up rather than shrinking ours down. */
.small {
    font-size: 1em;
}

/* dynamic/boards.dat and dynamic/ads.dat embed their own caption markup with an inline
   style="position:relative;top:-18px;..." - a hack that pulled the caption up to
   overlap the box's bottom border in production's old tight document-flow layout. In
   this grid layout, spacing between a box and its caption already comes from the grid
   gap, so that same -18px offset just shifts the caption to the wrong spot instead
   (visually too close to / overlapping the graphic above it). An inline style attribute
   normally beats any stylesheet rule regardless of specificity, so this needs
   !important - a deliberate, narrow exception, not a habit. Left color alone since
   that part isn't broken. */
.area-ads span[style] {
    position: static !important;
    top: auto !important;
}

/* These captions live in their own grid row (the box and caption are unpaired sibling
   elements straight from the data file, not grouped in a .panel-item like our own
   captions are), so their box-to-caption spacing comes from the grid's 10px gap - 12px
   measured in practice - instead of the 4px margin every one of our own captions uses.
   Pulling the row up by the difference (8px) matches the two exactly rather than leaving
   a visible "some captions sit lower than others" inconsistency. */
.area-ads center:has(> span[style]) {
    margin-top: -8px;
}

/* These are pixel-art banners carrying legacy width="" attributes (150-330px) sized for
   production's narrower/differently-proportioned columns. Most are natively larger than
   that anyway (e.g. info.jpg is 282x70 native, forced down to 150) - scaling to fill the
   panel's actual width looks crisper, not blurrier, and doesn't leave dead space beside
   a small fixed-size image in a wider box. image-rendering keeps pixel art crisp on the
   few images (whatsnew.png) that do scale past their native size. */
.panel img, .box img {
    display: block;
    width: 100%;
    height: auto;
    margin: 0 auto 6px;
    image-rendering: pixelated;
}

.panel-item {
    margin: 0;
}

.panel-caption {
    /* Was 0.85em (13.6px) - smaller than the dat-driven captions from ads.dat/boards.dat
       (16px, unstyled default, no font-size of their own). Matched up to the larger of
       the two instead of shrinking those down, so every caption is consistent. */
    text-align: center;
    font-size: 1em;
    color: var(--fg-dim);
    margin: 4px 0 0 0;
}

.panel-caption a {
    color: var(--fg-dim);
    font-weight: normal;
}

.panel-caption a:hover {
    color: var(--accent-green);
}

.panel table {
    width: 100%;
    border-collapse: collapse;
    table-layout: auto;
}

/* The BBS-data modules (lastcalls.php, whosonline.php, etc.) carry legacy fixed-pixel
   width="" HTML attributes on <table>/<td>, sized for a wider container than any of our
   panels use. A plain author-stylesheet rule already beats those (HTML width="" is a
   "presentational hint" with the lowest possible cascade priority) - this lets the
   browser compute widths from actual content so tables fit their column and wrap
   sensibly instead of overflowing it. */
.panel td, .panel th {
    width: auto;
    padding: 1px 4px;
    overflow-wrap: anywhere;
}

.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.blog-banner {
    display: block;
    width: 100%;
    max-width: 725px;
    height: auto;
    margin: 0 0 10px 0;
    image-rendering: pixelated;
}

/* story.php's inline photos - production floated these with the deprecated align=""
   attribute; same visual effect via a real CSS float, with text wrapping around them
   same as before. */
/* Native size is only 225-250px wide - max-width alone was just a ceiling, nothing was
   actually forcing them to grow to fill it, so they rendered at their tiny native pixel
   size regardless. width forces them to actually reach that size instead of just
   capping how big they're allowed to get. image-rendering keeps them crisp at the
   larger size, same treatment as the other pixel-art graphics on the site. */
.story-img {
    width: 33%;
    max-width: 33%;
    height: auto;
    border: 1px solid var(--border);
    image-rendering: pixelated;
}

.story-img--left {
    float: left;
    margin: 0 16px 12px 0;
}

.story-img--right {
    float: right;
    margin: 0 0 12px 16px;
}

.story-post::after {
    content: '';
    display: table;
    clear: both;
}

/* ---- Footer ---- */

.site-footer {
    background: var(--nav-bg);
    color: #f0f0f0;
    text-align: center;
    /* Sized to match .site-nav a's proportions (1em padding, 1em font-size) rather than
       its own previous, much smaller values (8px, 0.9em) - the footer read as an
       afterthought next to the nav bar. */
    padding: 1em 0;
    font-size: 1em;
}

.site-footer a {
    color: #f0f0f0;
}

.site-footer a:hover {
    color: var(--accent-green);
    text-decoration: underline;
}

/* ---- CRT / retro effects ----
   Scanlines + vignette on a single fixed full-viewport overlay (one element, two
   layered backgrounds - cheap to paint, doesn't affect layout since it's pointer-events:
   none and out of the document flow). Phosphor glow on headers/accents only, not on
   muted body text, so it doesn't cost readability. Everything animated respects
   prefers-reduced-motion. */

body::after {
    content: '';
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 999;
    background:
        repeating-linear-gradient(
            to bottom,
            rgba(0, 0, 0, 0.15) 0,
            rgba(0, 0, 0, 0.15) 1px,
            rgba(0, 0, 0, 0) 2px,
            rgba(0, 0, 0, 0) 3px
        ),
        radial-gradient(
            ellipse at center,
            rgba(0, 0, 0, 0) 55%,
            rgba(0, 0, 0, 0.4) 100%
        );
    animation: crt-flicker 7s infinite;
}

@keyframes crt-flicker {
    0%, 100% { opacity: 1; }
    92% { opacity: 1; }
    93% { opacity: 0.94; }
    94% { opacity: 1; }
    96% { opacity: 0.97; }
    97% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    body::after {
        animation: none;
    }
}

h1, h2, .site-nav a[aria-current="page"], .site-nav a:hover {
    text-shadow: 0 0 5px currentColor;
}

/* Blinking terminal cursor - drop <span class="cursor" aria-hidden="true"></span>
   anywhere a line should end like it's waiting for input. */
.cursor::after {
    content: '\2588';
    display: inline-block;
    margin-left: 2px;
    animation: cursor-blink 1s steps(1) infinite;
}

@keyframes cursor-blink {
    0%, 50% { opacity: 1; }
    50.01%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .cursor::after {
        animation: none;
    }
}

