A tiny read-only git web frontend — browse bare repos with just PHP and git, no database, no framework.
/* foxygit — structure only, no colors. All colors come from a theme file
in themes/*.css via CSS custom properties (--bg, --fg, --dim, --acc,
--line, --surface, --pre-bg, --add, --del, --hunk, --font-ui). */
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font: 14px/1.5 var(--font-ui);
-webkit-font-smoothing: antialiased;
}
a { color: var(--acc); text-decoration: none; }
a:hover { text-decoration: underline; }
/* code/hashes/diffs always render monospace, regardless of --font-ui */
pre, code, .mono, .hash {
font-family: ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
}
/* middle, not text-bottom: table cells set their own line-height, and
text-bottom pushed icons visibly below the row's text baseline. */
.icon { fill: currentColor; vertical-align: middle; flex: none; }
/* the shared centred column — header, tabs and main all line up on it */
.inner { max-width: 1100px; margin: 0 auto; padding: 0 24px; width: 100%; }
/* ---------------------------------------------------------------- header */
header {
border-bottom: 1px solid var(--line);
background: var(--surface);
padding: 12px 0;
}
header .inner { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; }
header .brand { color: var(--fg); font-weight: 600; font-size: 16px; display: inline-flex; align-items: center; gap: 8px; }
header .brand .icon { color: var(--dim); }
header .brand:hover { text-decoration: none; }
header .path { color: var(--dim); display: inline-flex; align-items: center; gap: 6px; font-size: 16px; }
header .path a { color: var(--fg); font-weight: 600; }
header .spacer { flex: 1; }
/* also used on a <button> (the header's log-out form), so it has to unset the
background the button rule further down would otherwise give it — every
icon button in the header sits flush until hovered. */
.icon-btn {
color: var(--dim);
background: none;
border: 1px solid transparent;
border-radius: 6px;
padding: 5px;
display: inline-flex;
line-height: 0;
cursor: pointer;
}
.icon-btn:hover { color: var(--fg); background: var(--bg); border-color: var(--line); text-decoration: none; }
main { padding: 24px 0 0; }
/* ----------------------------------------------------------------- tabs */
nav.tabs { border-bottom: 1px solid var(--line); }
nav.tabs .inner { display: flex; gap: 4px; }
nav.tabs a {
color: var(--dim);
padding: 10px 12px;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
display: inline-flex;
align-items: center;
gap: 8px;
}
nav.tabs a:hover { color: var(--fg); text-decoration: none; }
nav.tabs a.on { color: var(--fg); font-weight: 600; border-bottom-color: var(--acc); }
/* --------------------------------------------------------------- subnav:
branch pills + clone URL + feed links, the row above the content. */
.repo-subnav {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.repo-subnav .branches { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.repo-subnav .branches .icon { color: var(--dim); }
.clone-box {
background: var(--pre-bg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 5px 10px;
display: inline-flex;
gap: 8px;
align-items: center;
font-size: 12px;
max-width: 100%;
overflow: hidden;
color: var(--dim);
font-family: inherit;
cursor: pointer;
}
.clone-box:hover { border-color: var(--acc); color: var(--fg); }
.clone-box code { color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clone-box .done-icon { display: none; }
.clone-box.copied { color: var(--add); border-color: var(--add); }
.clone-box.copied .copy-icon { display: none; }
.clone-box.copied .done-icon { display: inline-flex; }
.feeds { margin-left: auto; color: var(--dim); font-size: 12px; display: flex; gap: 10px; align-items: center; }
.feeds a { color: var(--dim); }
.feeds a:hover { color: var(--fg); }
/* ---------------------------------------------------------------- tables */
table { width: 100%; border-collapse: collapse; }
td, th { text-align: left; padding: 8px 16px 8px 0; vertical-align: middle; }
th { color: var(--dim); font-weight: 400; font-size: 12px; }
tr:hover td { background: var(--surface); }
.desc { color: var(--dim); }
.num { text-align: right; white-space: nowrap; }
/* ------------------------------------------------------------------ .box:
the card wrapper used around file/ref/log tables and the README. */
.box {
border: 1px solid var(--line);
border-radius: 6px;
overflow: hidden;
margin-bottom: 16px;
}
.box table { margin: 0; }
.box td, .box th { padding-left: 16px; }
.box tr { border-top: 1px solid var(--line); }
.box tr:first-child { border-top: 0; }
.box.about { padding: 14px 16px; color: var(--fg); }
.box thead th {
background: var(--surface);
border-bottom: 1px solid var(--line);
padding-top: 10px;
padding-bottom: 10px;
}
.box thead tr:hover td { background: none; }
/* file tree: type icon, name, right-aligned size.
The icon is display:block so it isn't treated as inline text — as an inline
element an SVG sits on the baseline and reserves descender space beneath it,
which is what pushed it below the filename. The cell centres it instead. */
.tree td.mode { width: 1%; padding-right: 0; color: var(--dim); }
.tree td.mode .icon { display: block; }
.tree td.num { width: 1%; color: var(--dim); font-size: 12px; }
.tree td.commit-msg { max-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; }
.tree td.commit-msg a { color: inherit; }
.tree td.commit-msg a:hover { color: var(--acc); }
.tree tbody a { color: var(--fg); }
.tree tbody a:hover { color: var(--acc); }
.box-header {
background: var(--surface);
border-bottom: 1px solid var(--line);
padding: 10px 16px;
font-size: 12px;
color: var(--dim);
display: flex;
align-items: center;
gap: 8px;
}
/* --------------------------------------------------------------- pills:
commit hashes, branch names — small rounded chips on an inset bg. */
.pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--pre-bg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 2px 8px;
font-size: 12px;
color: var(--dim);
}
a.pill:hover { text-decoration: none; border-color: var(--acc); color: var(--fg); }
a.pill.on { color: var(--fg); border-color: var(--acc); }
/* ----------------------------------------------------------------- diff */
pre {
background: var(--pre-bg);
border: 1px solid var(--line);
padding: 12px;
overflow: auto;
border-radius: 6px;
}
.add { color: var(--add); }
.del { color: var(--del); }
.hunk { color: var(--hunk); }
/* --------------------------------------------------------------- readme */
.readme .box-header { color: var(--fg); font-weight: 600; }
.readme .body { padding: 24px 32px; }
.readme h1, .readme h2, .readme h3, .readme h4, .readme h5, .readme h6 {
color: var(--fg); margin: 24px 0 12px; scroll-margin-top: 16px;
}
.readme h1 { border-bottom: 1px solid var(--line); padding-bottom: .3em; font-size: 26px; }
.readme h2 { border-bottom: 1px solid var(--line); padding-bottom: .3em; font-size: 20px; }
.readme h1:first-child, .readme h2:first-child { margin-top: 0; }
.readme p { margin: 12px 0; }
.readme ul, .readme ol { margin: 12px 0; padding-left: 22px; }
.readme li { margin: 4px 0; }
.readme li > ul, .readme li > ol { margin: 4px 0; }
.readme code { background: var(--surface); padding: 2px 6px; border-radius: 6px; font-size: 85%; }
.readme pre { background: var(--pre-bg); }
.readme pre code { background: none; padding: 0; font-size: 100%; }
.readme img { max-width: 100%; height: auto; }
.readme del { color: var(--dim); }
.readme hr { border: 0; border-top: 1px solid var(--line); margin: 24px 0; }
.readme blockquote {
margin: 12px 0;
padding: 1px 1em;
color: var(--dim);
border-left: .25em solid var(--line);
}
.readme blockquote > *:first-child { margin-top: 0; }
.readme blockquote > *:last-child { margin-bottom: 0; }
.readme table { display: block; width: max-content; max-width: 100%; overflow: auto; margin: 12px 0; border-collapse: collapse; }
.readme table th, .readme table td { border: 1px solid var(--line); padding: 6px 12px; }
.readme table tr:nth-child(2n) td { background: var(--surface); }
.license-line { color: var(--dim); font-size: 12px; margin: 0 0 16px; }
/* --------------------------------------------------------------- blob view */
.blob-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.blob-toolbar .desc { margin: 0; }
.blob-toolbar .spacer { flex: 1; }
.img-frame { padding: 32px; text-align: center; }
.img-frame img { max-width: 100%; max-height: 70vh; border-radius: 4px; }
/* ------------------------------------------------------------- repo list
Real <table> markup, restyled as a stack of bordered cards via CSS grid
— keeps it a semantic table while looking like GitHub's repo list. */
#repo-list { display: block; }
#repo-list tbody, #repo-list tr { display: block; }
#repo-list tr {
border: 1px solid var(--line);
border-radius: 6px;
padding: 16px;
margin-bottom: 12px;
display: grid;
grid-template-columns: 1fr auto;
gap: 4px 16px;
}
#repo-list tr:hover td { background: none; }
#repo-list td { padding: 0; border: 0; }
#repo-list td:nth-child(1) { grid-row: 1; font-size: 16px; font-weight: 600; }
#repo-list td:nth-child(2) { grid-column: 1 / 2; grid-row: 2; color: var(--dim); font-size: 13px; }
#repo-list td:nth-child(3) { grid-column: 2 / 3; grid-row: 1; color: var(--dim); font-size: 12px; white-space: nowrap; }
#repo-list td:nth-child(4) { grid-column: 1 / -1; grid-row: 3; }
/* the language bar: a row of proportionally-sized colored segments, GitHub-style,
plus a wrapping text legend (colored dot + name + percentage) underneath. */
.lang-bar {
display: flex;
width: 25%;
height: 6px;
border-radius: 3px;
overflow: hidden;
background: var(--surface);
margin-top: 8px;
}
.lang-bar span { display: block; height: 100%; }
.lang-legend {
display: flex;
flex-wrap: wrap;
gap: 4px 14px;
margin-top: 6px;
font-size: 12px;
color: var(--dim);
}
.lang-legend-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.lang-legend-item i { width: 9px; height: 9px; display: inline-block; flex: none; }
/* ------------------------------------------------------------------ log */
.log-date {
padding: 10px 16px;
background: var(--surface);
color: var(--dim);
font-size: 12px;
border-top: 1px solid var(--line);
}
.log-date:first-child { border-top: 0; }
.box table td.subject { font-weight: 500; }
/* --------------------------------------------------------------- footer */
footer {
color: var(--dim);
border-top: 1px solid var(--line);
margin-top: 40px;
padding: 24px 0;
font-size: 12px;
}
footer .inner { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.theme-switch { display: inline-flex; align-items: center; gap: 6px; }
.theme-switch select {
background: var(--surface);
color: var(--fg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 3px 6px;
font: inherit;
font-size: 12px;
}
/* ------------------------------------------------------------ auth nav:
the account / admin / log-out controls. flex-basis:100% inside the
header's wrapping .inner puts them on their OWN row under the brand and
icon buttons, rather than competing for the corner — squeezed in beside
the theme and help icons they wrapped raggedly, with the log-out button
dropping under the username. nowrap keeps the three together on that row.
Colours are set explicitly (not via --pre-bg, whose job is code insets)
so the chips read the same in every theme. */
.nav-auth {
flex-basis: 100%;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: flex-end;
gap: 10px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--line);
}
.nav-auth form { display: inline-flex; margin: 0; }
/* one shared look for all three, with a fixed height so they line up exactly */
.nav-btn {
display: inline-flex;
align-items: center;
gap: 7px;
height: 30px;
padding: 0 12px;
background: var(--surface);
color: var(--fg);
border: 1px solid var(--line);
border-radius: 6px;
font: inherit;
font-size: 13px;
line-height: 1;
white-space: nowrap;
cursor: pointer;
transition: border-color .12s, color .12s;
}
.nav-btn:hover { border-color: var(--acc); color: var(--acc); text-decoration: none; }
.nav-btn:focus-visible {
outline: none;
border-color: var(--acc);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--acc) 15%, transparent);
}
.nav-btn .icon { color: var(--dim); }
.nav-btn:hover .icon { color: inherit; }
.nav-sep { width: 1px; height: 18px; background: var(--line); flex: none; }
/* ----------------------------------------------------------- page titles:
setup/login/account/admin are the only pages with a bare <h1> — every
other view relies on the header/tab bar for context instead. Kept light
(not a browser-default giant serif heading) so it sits at home next to
the rest of the 14px UI; can hold a leading .icon (h1's flex+gap centre
it against the text the same way box-header's icon does). */
h1 {
display: flex;
align-items: center;
gap: 10px;
font-size: 20px;
font-weight: 600;
color: var(--fg);
margin: 4px 0 20px;
}
/* --------------------------------------------------------------- forms:
login/setup/account/admin — the only pages in foxygit with real forms. */
.form { max-width: 420px; }
.form-row { margin-bottom: 14px; }
.form-row:last-of-type { margin-bottom: 18px; }
.form-row label,
.form > label { display: block; margin-bottom: 5px; color: var(--dim); font-size: 12px; }
/* Capped rather than 100%: these forms sit inside full-width cards, and a
text box stretched across the whole 1100px column looks broken. The auth
pages (narrow, centred) opt back into full width further down. */
input[type="text"], input[type="password"] {
width: 100%;
max-width: 320px;
background: var(--pre-bg); /* inset, like .pill and .clone-box */
color: var(--fg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 7px 10px;
font: inherit;
font-size: 13px;
}
input[type="text"]::placeholder, input[type="password"]::placeholder { color: var(--dim); }
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: var(--acc);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--acc) 15%, transparent);
}
/* input + its submit button side by side — the single-field "create" forms */
.field-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.field-row button { flex: none; }
/* one-line note under a field, explaining the rule the input enforces */
.field-hint { margin: 10px 0 0; font-size: 12px; }
.field-hint code { background: var(--pre-bg); border-radius: 4px; padding: 1px 5px; }
.check-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--fg); }
.check-row label { margin: 0; color: var(--fg); font-size: 13px; display: inline-flex; align-items: center; gap: 8px; }
input[type="checkbox"] { accent-color: var(--acc); width: 15px; height: 15px; margin: 0; flex: none; }
/* Buttons stay in the site's quiet register: thin border, inset fill, accent
carried as *colour* rather than a saturated slab. --acc is a pale mint, so
a solid fill of it reads as a glaring lozenge on a near-black page; a
tinted ghost keeps the primary action obvious without shouting.
(The plain background: is the fallback for anything without color-mix.) */
button, .btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: var(--surface);
color: var(--fg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 7px 14px;
font: inherit;
font-size: 13px;
line-height: 1.4;
cursor: pointer;
transition: border-color .12s, background .12s, color .12s;
}
button:hover, .btn:hover { border-color: var(--acc); color: var(--acc); text-decoration: none; }
button:focus-visible, .btn:focus-visible {
outline: none;
border-color: var(--acc);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--acc) 15%, transparent);
}
button.primary, .btn.primary {
color: var(--acc);
font-weight: 600;
background: var(--surface);
background: color-mix(in srgb, var(--acc) 13%, var(--surface));
border-color: var(--line);
border-color: color-mix(in srgb, var(--acc) 45%, var(--line));
}
button.primary:hover, .btn.primary:hover {
background: color-mix(in srgb, var(--acc) 22%, var(--surface));
border-color: var(--acc);
color: var(--acc);
}
button.danger, .btn.danger { color: var(--del); }
button.danger:hover, .btn.danger:hover {
border-color: var(--del);
background: color-mix(in srgb, var(--del) 14%, var(--surface));
color: var(--del);
}
.inline-form { display: inline-flex; gap: 6px; align-items: center; }
.msg-error, .msg-ok {
display: flex;
gap: 10px;
border-radius: 6px;
padding: 12px 14px;
margin-bottom: 16px;
border: 1px solid;
}
.msg-error .icon, .msg-ok .icon { flex: none; margin-top: 1px; }
.msg-error { color: var(--del); background: var(--pre-bg); border-color: var(--del); }
.msg-ok { color: var(--add); background: var(--pre-bg); border-color: var(--add); }
.msg-error > div, .msg-ok > div { color: var(--fg); }
.msg-ok p:first-child { margin-top: 0; }
.msg-ok p:last-child { margin-bottom: 0; }
.key-reveal { margin: 10px 0; }
.key-reveal code {
display: block;
background: var(--pre-bg);
border: 1px solid var(--line);
border-radius: 6px;
padding: 8px 10px;
word-break: break-all;
}
/* The "create" forms get their OWN card (box-header + form), rather than
being tacked onto the bottom of the list they add to — so the box-header's
own bottom rule is the only divider needed above them. */
.box .form {
max-width: none;
padding: 16px;
margin: 0;
}
.box .form-row:last-of-type { margin-bottom: 14px; }
.box .form .field-row:last-child,
.box .form > label + .field-row { margin-bottom: 0; }
/* ------------------------------------------------------------ row-actions:
the trash-icon disclosure used for delete-repo/delete-user — a plain
<details>/<summary>, no JS, so the confirm field only appears once you've
deliberately opened it instead of cluttering every row by default. */
.row-actions summary {
list-style: none;
display: inline-flex;
color: var(--dim);
border: 1px solid transparent;
border-radius: 6px;
padding: 5px;
cursor: pointer;
}
.row-actions summary::-webkit-details-marker { display: none; }
.row-actions summary:hover { color: var(--del); background: var(--bg); border-color: var(--line); }
.row-actions[open] summary { color: var(--del); background: var(--bg); border-color: var(--line); }
.row-actions .confirm-delete {
display: flex;
gap: 6px;
justify-content: flex-end;
margin-top: 8px;
}
.row-actions .confirm-delete input[type="text"] { width: 210px; max-width: 210px; }
/* --------------------------------------------------------- auth pages:
setup/login — a single centred card, the one place in foxygit that
looks like a dedicated screen rather than a page under the header. */
.auth-page { max-width: 360px; margin: 56px auto 0; }
.auth-page h1 { justify-content: center; font-size: 22px; margin-bottom: 6px; }
.auth-page .lede { text-align: center; margin: 0 0 24px; }
.auth-page .box { padding: 26px 26px 4px; background: var(--surface); }
.auth-page .form { max-width: none; padding: 0; background: none; border-top: 0; }
/* narrow, centred column — here full-width fields are the right call */
.auth-page input[type="text"], .auth-page input[type="password"] { max-width: none; }
.auth-page button.primary { width: 100%; padding: 9px 14px; }
@media (max-width: 720px) {
.inner { padding: 0 16px; }
.feeds { margin-left: 0; }
.nav-auth { justify-content: flex-start; gap: 8px; }
.nav-btn { padding: 0 10px; }
input[type="text"], input[type="password"] { max-width: none; }
.row-actions .confirm-delete { flex-wrap: wrap; }
}