add footer

This commit is contained in:
Joey Kimsey
2026-09-12 15:16:50 -04:00
parent 58ca586d94
commit ded1e1e069
5 changed files with 475 additions and 59 deletions

View File

@ -13,4 +13,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Password sign-in through `POST /api/login` and optional connect-with-token from Admin ? Tokens. MFA accounts continue in-app (`api/login/mfa/{loginCode}`) until a token is issued. `loginCode` stays in the main process; a pasted Admin token skips MFA. - Password sign-in through `POST /api/login` and optional connect-with-token from Admin ? Tokens. MFA accounts continue in-app (`api/login/mfa/{loginCode}`) until a token is issued. `loginCode` stays in the main process; a pasted Admin token skips MFA.
- Session stored in `userData`, encrypted with `safeStorage` when the OS allows it. The renderer never receives the token. - Session stored in `userData`, encrypted with `safeStorage` when the OS allows it. The renderer never receives the token.
- Logged-in chrome matches TTP: navy header, Font Awesome 6.7.1 / Bootstrap 5.3, centered full-width search, notifications and messages dropdowns, avatar account menu. Profile settings (avatar, name, gender, newsletter, timezone, date/time, page size, dark mode) live in-app and save through `POST /api/profile/update`. Email, password, and phone open the connected site. - Logged-in chrome matches TTP: navy header, Font Awesome 6.7.1 / Bootstrap 5.3, centered full-width search, notifications and messages dropdowns, avatar account menu. Profile settings (avatar, name, gender, newsletter, timezone, date/time, page size, dark mode) live in-app and save through `POST /api/profile/update`. Email, password, and phone open the connected site.
- After sign-in, Capsule loads `GET /api/profile` plus notifications and messages. Search, contact, and bug reports use the matching user-token endpoints. Disabled plugins show an unavailable note instead of demo data. - After sign-in, Capsule loads `GET /api/profile` plus notifications and messages. Search uses the matching user-token endpoint. Contact and bug reports live on footer pages (`#/contact`, `#/bugreport`) instead of the dashboard. Disabled plugins show an unavailable note instead of demo data.
- Footer chrome matches TTP copy and socials. The upper band keeps a dark-mode toggle, Privacy Policy, Terms of Service, Contact, and Report a Bug. There is no subscribe box.

View File

@ -29,7 +29,7 @@ All HTTP runs in the **main process**. The renderer never sees the token and nev
| Search | `GET /api/search` | Header search. `q`, `resource`, `page`. | | Search | `GET /api/search` | Header search. `q`, `resource`, `page`. |
| Profile save | `POST /api/profile/update` | Name, avatar, prefs. | | Profile save | `POST /api/profile/update` | Name, avatar, prefs. |
| Mail / notices | `POST /api/messages/?`, `POST /api/notifications/?` | View, reply, create, read, delete. | | Mail / notices | `POST /api/messages/?`, `POST /api/notifications/?` | View, reply, create, read, delete. |
| Contact / bugs | `POST /api/contact`, `POST /api/bugreport` | Dashboard forms when those plugins are enabled. | | Contact / bugs | `POST /api/contact`, `POST /api/bugreport` | Footer pages when those plugins are enabled. |
| Existing token | Admin ? Tokens | Personal or app token. A user token hydrates the workspace; an app token can connect but cannot call the user API. | | Existing token | Admin ? Tokens | Personal or app token. A user token hydrates the workspace; an app token can connect but cannot call the user API. |
The token is stored under Electron `userData` (`session.json`). `safeStorage` encrypts it when the OS keychain is available. MFA `loginCode` is not stored. The token is stored under Electron `userData` (`session.json`). `safeStorage` encrypts it when the OS keychain is available. MFA `loginCode` is not stored.
@ -44,7 +44,7 @@ App-facing pairing notes live with the PHP app: `repos/ttp/docs/capsule.md`.
| `src/preload/` | `window.capsule` bridge | | `src/preload/` | `window.capsule` bridge |
| `src/renderer/` | Login, MFA, TTP-styled chrome, and live API views | | `src/renderer/` | Login, MFA, TTP-styled chrome, and live API views |
The logged-in header follows the public TTP shell (`text-bg-dark`, FA 6.7.1, Bootstrap 5.3). Search stays visible and centered. Account is a top-right dropdown like the site. Notifications and messages are the same bell / envelope menus. Profile edit covers User CP preferences except email, password, and phone ? those open `{site}/usercp/?`. Lists load from the site API after sign-in. The logged-in header follows the public TTP shell (`text-bg-dark`, FA 6.7.1, Bootstrap 5.3). Search stays visible and centered. Account is a top-right dropdown like the site. Notifications and messages are the same bell / envelope menus. Profile edit covers User CP preferences except email, password, and phone ? those open `{site}/usercp/?`. Lists load from the site API after sign-in. The footer matches TTP copyright and social icons. Above that: dark-mode toggle, Privacy Policy, Terms of Service, Contact, and Report a Bug. No subscribe box. Privacy and terms open the connected site; contact and bug reports stay in-app.
## Remote ## Remote

View File

@ -344,59 +344,171 @@
<div class="m-2 m-lg-4"> <div class="m-2 m-lg-4">
<div class="col-12 col-lg-10 mx-auto p-4 rounded shadow-sm context-main-bg"> <div class="col-12 col-lg-10 mx-auto p-4 rounded shadow-sm context-main-bg">
<h1 class="h3">Dashboard</h1> <h1 class="h3">Dashboard</h1>
<h1 class="h3">Workspace</h1> <p class="text-muted" id="home-note"></p>
<p class="text-muted" id="home-note">
Search, notifications, messages, and profile are here. Site API wiring is the next
step.
</p>
<p class="mb-0"> <p class="mb-0">
Connected to <a id="home-site" href="#" target="_blank" rel="noreferrer"></a> Connected to <a id="home-site" href="#" target="_blank" rel="noreferrer"></a>
as <strong id="home-username"></strong>. as <strong id="home-username"></strong>.
</p> </p>
<p id="home-plugins" class="small text-muted mb-4"></p> <p id="home-plugins" class="small text-muted mb-0"></p>
<div class="row g-4"> </div>
<div class="col-md-6" id="contact-panel" hidden> </div>
<h2 class="h5">Contact</h2> </section>
<form id="contact-form">
<div class="mb-2"> <section id="view-contact" class="view" hidden>
<label class="form-label" for="contact-name">Name</label> <div class="m-2 m-lg-4">
<input id="contact-name" class="form-control" name="name" type="text" required /> <div class="context-main-bg container py-2 my-2 py-lg-4 my-lg-4">
</div> <h1 class="h2 text-center mb-4">Contact Us</h1>
<div class="mb-2"> <p id="contact-unavailable" class="text-muted text-center" hidden></p>
<label class="form-label" for="contact-email">Email</label> <div id="contact-body">
<input id="contact-email" class="form-control" name="email" type="email" /> <div class="col-12 col-lg-6 offset-lg-3">
</div> <p class="text-center text-lg-start">
<div class="mb-2"> Here at <strong id="contact-sitename">this site</strong>, we highly value your
<label class="form-label" for="contact-entry">Message</label> feedback. We constantly strive to provide our users with the highest level of
<textarea id="contact-entry" class="form-control" name="entry" rows="3" required></textarea> quality in everything we do.
</div> </p>
<p id="contact-status" class="small" hidden></p> <p class="text-center text-lg-start">
<button class="btn btn-primary" type="submit">Send</button> If you would like to provide any suggestions or comments on our service, we ask
</form> that you please fill out the quick form below and let us know what's on your mind.
</p>
</div> </div>
<div class="col-md-6" id="bugreport-panel" hidden> <form id="contact-form">
<h2 class="h5">Bug report</h2> <div class="mb-3 row">
<form id="bugreport-form"> <label for="contact-name" class="col-lg-3 col-form-label text-lg-end">Name:</label>
<div class="mb-2"> <div class="col-lg-6">
<label class="form-label" for="bug-url">Page URL</label> <input
<input id="bug-url" class="form-control" name="url" type="url" required /> id="contact-name"
class="form-control"
name="name"
type="text"
required
autocomplete="name"
/>
</div> </div>
<div class="mb-2"> </div>
<label class="form-label" for="bug-ourl">Expected URL <span class="text-muted">(optional)</span></label> <div class="mb-3 row">
<input id="bug-ourl" class="form-control" name="ourl" type="url" /> <label for="contact-email" class="col-lg-3 col-form-label text-lg-end">
E-mail: (optional)
</label>
<div class="col-lg-6">
<input
id="contact-email"
class="form-control"
name="email"
type="email"
autocomplete="email"
/>
</div> </div>
<div class="form-check mb-2"> </div>
<input class="form-check-input" type="checkbox" id="bug-repeat" /> <div class="mb-3 row">
<label class="form-check-label" for="bug-repeat">This happens every time</label> <label for="contact-entry" class="col-lg-3 col-form-label text-lg-end">Feedback:</label>
<div class="col-lg-6">
<textarea
id="contact-entry"
class="form-control"
name="entry"
rows="6"
maxlength="2000"
required
></textarea>
<small class="form-text text-muted">Max: 2000 characters</small>
</div> </div>
<div class="mb-2"> </div>
<label class="form-label" for="bug-entry">What happened</label> <p id="contact-status" class="small text-center" hidden></p>
<textarea id="bug-entry" class="form-control" name="entry" rows="3" required></textarea> <div class="text-center">
<button class="btn btn-primary btn-lg" type="submit">Submit</button>
</div>
</form>
</div>
</div>
</div>
</section>
<section id="view-bugreport" class="view" hidden>
<div class="m-2 m-lg-4">
<div class="col-12 col-sm-10 col-lg-8 mx-auto p-4 rounded shadow-sm context-main-bg">
<h1 class="h2 text-center mb-4">Report a Bug</h1>
<hr />
<p id="bug-unavailable" class="text-muted text-center" hidden></p>
<div id="bug-body">
<p class="text-center text-sm-start">
Thank you for visiting our bug reporting page. We value our users' input highly and
in an effort to better serve your needs, please fill out the form below to help us
address this issue.
</p>
<p class="text-center text-sm-start">
We read each and every bug report submitted, and by submitting this form you allow
us to send you a follow-up email.
</p>
<form id="bugreport-form">
<div class="mb-3">
<label class="form-label" for="bug-url">Page you were trying to reach:</label>
<input
id="bug-url"
class="form-control"
name="url"
type="url"
aria-describedby="bug-url-help"
required
/>
<small id="bug-url-help" class="form-text text-muted">
This is the URL of the page you actually received the error.
</small>
</div>
<div class="mb-3">
<label class="form-label" for="bug-ourl">Page you were on:</label>
<input
id="bug-ourl"
class="form-control"
name="ourl"
type="url"
aria-describedby="bug-ourl-help"
/>
<small id="bug-ourl-help" class="form-text text-muted">
This is the URL of the page you were on before you received the error.
</small>
</div>
<div class="mb-3">
<p class="form-label">*Has this happened more than once?</p>
<div class="form-check">
<input
class="form-check-input"
type="radio"
name="repeat"
id="bug-repeat-no"
value="false"
checked
/>
<label class="form-check-label" for="bug-repeat-no">No</label>
</div> </div>
<p id="bug-status" class="small" hidden></p> <div class="form-check">
<button class="btn btn-primary" type="submit">Send report</button> <input
</form> class="form-check-input"
</div> type="radio"
name="repeat"
id="bug-repeat-yes"
value="true"
/>
<label class="form-check-label" for="bug-repeat-yes">Yes</label>
</div>
</div>
<div class="mb-3">
<label class="form-label" for="bug-entry">Describe the error you received:</label>
<textarea
id="bug-entry"
class="form-control"
name="entry"
rows="6"
maxlength="2000"
aria-describedby="bug-entry-help"
required
></textarea>
<small id="bug-entry-help" class="form-text text-muted">(max: 2000 characters)</small>
</div>
<p id="bug-status" class="small text-center" hidden></p>
<div class="text-center">
<button class="btn btn-primary btn-lg" type="submit">Submit</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -620,6 +732,116 @@
</section> </section>
</main> </main>
<div class="container mt-auto">
<footer class="pt-4">
<div class="text-center border-top context-main-border">
<button
class="d-md-none my-3 btn btn-lg context-main context-main-border"
type="button"
data-bs-toggle="collapse"
data-bs-target="#footerMenu"
aria-controls="footerMenu"
aria-expanded="false"
aria-label="Toggle footer navigation"
>
<i class="fa fa-bars"></i>
</button>
</div>
<div class="collapse d-md-block my-4" id="footerMenu">
<div class="row">
<div id="footer-contact-col" class="col-12 col-sm-6 col-md-3 col-lg-2 mb-3 text-center">
<h2 class="h5">Contact Us</h2>
<ul class="nav flex-column">
<li id="footer-link-contact" class="nav-item mb-2">
<a href="#/contact" class="nav-link p-0 text-muted">Contact</a>
</li>
<li id="footer-link-bug" class="nav-item mb-2">
<a href="#/bugreport" class="nav-link p-0 text-muted">Report a Bug</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-2 mb-3 text-center">
<h2 class="h5">More Info</h2>
<ul class="nav flex-column">
<li class="nav-item mb-2">
<a
id="footer-privacy"
href="#"
class="nav-link p-0 text-muted"
target="_blank"
rel="noreferrer"
data-footer-path="home/privacy"
>Privacy Policy</a>
</li>
<li class="nav-item mb-2">
<a
id="footer-terms"
href="#"
class="nav-link p-0 text-muted"
target="_blank"
rel="noreferrer"
data-footer-path="home/terms"
>Terms of Service</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3 col-lg-2 mb-3 text-center">
<h2 class="h5">Dark Mode</h2>
<div class="material-switch px-4 mt-2">
<input name="dark-mode-toggle" type="checkbox" id="dark-mode-toggle" class="form-check-input" />
<label for="dark-mode-toggle" class="label-default"><span class="visually-hidden">Dark mode</span></label>
</div>
</div>
</div>
</div>
<div class="d-flex flex-column flex-md-row justify-content-md-between py-3 border-top context-main-border">
<div class="d-flex justify-content-center justify-content-md-start text-center text-md-start">
<span>
&copy; <span id="footer-year"></span>
<span id="footer-sitename">Capsule</span>, Powered by
<a
href="https://thetempusproject.com"
class="text-decoration-none"
target="_blank"
rel="noreferrer"
>The Tempus Project</a>
</span>
</div>
<div class="d-flex justify-content-center justify-content-md-end mt-3 mt-md-0">
<ul class="list-unstyled d-flex mb-0">
<li class="ms-3">
<a class="context-main" href="#" target="_blank" rel="noreferrer" data-footer-path="fb" aria-label="Facebook">
<span class="fa-brands fa-fw fa-facebook" aria-hidden="true"></span>
</a>
</li>
<li class="ms-3">
<a class="context-main" href="#" target="_blank" rel="noreferrer" data-footer-path="twitter" aria-label="X (Twitter)">
<span class="fa-brands fa-fw fa-twitter" aria-hidden="true"></span>
</a>
</li>
<li class="ms-3">
<a class="context-main" href="#" target="_blank" rel="noreferrer" data-footer-path="in" aria-label="LinkedIn">
<span class="fa-brands fa-fw fa-linkedin" aria-hidden="true"></span>
</a>
</li>
<li class="ms-3">
<a class="context-main" href="#" target="_blank" rel="noreferrer" data-footer-path="youtube" aria-label="YouTube">
<span class="fa-brands fa-fw fa-youtube" aria-hidden="true"></span>
</a>
</li>
<li class="ms-3">
<a class="context-main" href="#" target="_blank" rel="noreferrer" data-footer-path="git" aria-label="GitHub">
<span class="fa-brands fa-fw fa-github" aria-hidden="true"></span>
</a>
</li>
</ul>
</div>
</div>
</footer>
</div>
<script <script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous" crossorigin="anonymous"

View File

@ -22,7 +22,9 @@ const views = {
notifications: document.getElementById('view-notifications'), notifications: document.getElementById('view-notifications'),
messages: document.getElementById('view-messages'), messages: document.getElementById('view-messages'),
profile: document.getElementById('view-profile'), profile: document.getElementById('view-profile'),
settings: document.getElementById('view-settings') settings: document.getElementById('view-settings'),
contact: document.getElementById('view-contact'),
bugreport: document.getElementById('view-bugreport')
} }
const mainPages = { const mainPages = {
@ -124,10 +126,28 @@ function hasPlugin(name) {
* @return {string} - absolute URL, or # * @return {string} - absolute URL, or #
*/ */
function siteHref(path) { function siteHref(path) {
if (!session?.siteUrl) { const root = session?.siteUrl || session?.lastSiteUrl
if (!root) {
return '#' return '#'
} }
return `${session.siteUrl.replace(/\/+$/, '')}/${String(path).replace(/^\/+/, '')}` return `${root.replace(/\/+$/, '')}/${String(path).replace(/^\/+/, '')}`
}
/**
* Name for copyright and contact copy.
*
* @return {string}
*/
function displaySiteName() {
const root = session?.siteUrl || session?.lastSiteUrl
if (!root) {
return 'Capsule'
}
try {
return new URL(root).hostname.replace(/^www\./i, '')
} catch {
return 'Capsule'
}
} }
/** /**
@ -366,6 +386,36 @@ function renderChrome() {
) )
.join('') .join('')
) )
renderFooter()
}
/**
* Copyright, socials, legal links, and contact/bug items.
*
* @return {void}
*/
function renderFooter() {
document.getElementById('footer-year').textContent = String(new Date().getFullYear())
const sitename = displaySiteName()
document.getElementById('footer-sitename').textContent = sitename
document.getElementById('contact-sitename').textContent = sitename
document.querySelectorAll('[data-footer-path]').forEach((link) => {
const href = siteHref(link.dataset.footerPath)
link.href = href
if (href === '#') {
link.removeAttribute('target')
} else {
link.setAttribute('target', '_blank')
}
})
const showContact = Boolean(session?.connected) && (session.preview || hasPlugin('contact'))
const showBug = Boolean(session?.connected) && (session.preview || hasPlugin('bugreport'))
document.getElementById('footer-link-contact').hidden = !showContact
document.getElementById('footer-link-bug').hidden = !showBug
document.getElementById('footer-contact-col').hidden = !showContact && !showBug
} }
/** /**
@ -439,15 +489,21 @@ function renderPages() {
? `Enabled plugins: ${plugins.join(', ')}` ? `Enabled plugins: ${plugins.join(', ')}`
: 'Plugin list will show after profile loads.' : 'Plugin list will show after profile loads.'
const contactPanel = document.getElementById('contact-panel') const contactOk = session.preview || hasPlugin('contact')
const bugPanel = document.getElementById('bugreport-panel') const bugOk = session.preview || hasPlugin('bugreport')
contactPanel.hidden = session.preview || !hasPlugin('contact') document.getElementById('contact-body').hidden = !contactOk
bugPanel.hidden = session.preview || !hasPlugin('bugreport') document.getElementById('bug-body').hidden = !bugOk
if (!contactPanel.hidden && profile.name && !document.getElementById('contact-name').value) { const contactUnavailable = document.getElementById('contact-unavailable')
const bugUnavailable = document.getElementById('bug-unavailable')
contactUnavailable.hidden = contactOk
bugUnavailable.hidden = bugOk
contactUnavailable.textContent = contactOk ? '' : 'Contact is not available on this site.'
bugUnavailable.textContent = bugOk ? '' : 'Bug reports are not available on this site.'
if (contactOk && profile.name && !document.getElementById('contact-name').value) {
document.getElementById('contact-name').value = profile.name || profile.username || '' document.getElementById('contact-name').value = profile.name || profile.username || ''
document.getElementById('contact-email').value = profile.email || '' document.getElementById('contact-email').value = profile.email || ''
} }
if (!bugPanel.hidden && session.siteUrl && !document.getElementById('bug-url').value) { if (bugOk && session.siteUrl && !document.getElementById('bug-url').value) {
document.getElementById('bug-url').value = session.siteUrl document.getElementById('bug-url').value = session.siteUrl
} }
@ -602,7 +658,44 @@ function renderThread() {
* @return {void} * @return {void}
*/ */
function applyTheme(dark) { function applyTheme(dark) {
document.documentElement.setAttribute('data-bs-theme', dark ? 'dark' : 'light') const on = Boolean(dark)
document.documentElement.setAttribute('data-bs-theme', on ? 'dark' : 'light')
const footerToggle = document.getElementById('dark-mode-toggle')
const settingsToggle = document.getElementById('settings-dark')
if (footerToggle) {
footerToggle.checked = on
}
if (settingsToggle) {
settingsToggle.checked = on
}
}
/**
* Apply dark mode and save it on a live session.
*
* @param {boolean} dark - dark-mode preference
* @param {boolean} persist - POST api/profile/update when signed in
* @return {Promise<void>}
*/
async function setDarkMode(dark, persist) {
profile.darkMode = Boolean(dark)
applyTheme(profile.darkMode)
if (!persist || session?.preview || !window.capsule || !session?.connected) {
return
}
try {
const data = await runApi(() =>
window.capsule.updateProfile({
fields: { darkMode: profile.darkMode ? '1' : '0' }
})
)
if (data) {
applyProfile(data)
applyTheme(profile.darkMode)
}
} catch {
applyTheme(profile.darkMode)
}
} }
/** /**
@ -703,6 +796,14 @@ function route() {
showView('settings') showView('settings')
return return
} }
if (path === 'contact') {
showView('contact')
return
}
if (path === 'bugreport') {
showView('bugreport')
return
}
if (path === 'search') { if (path === 'search') {
showView('search') showView('search')
const params = new URLSearchParams(query || '') const params = new URLSearchParams(query || '')
@ -1096,6 +1197,11 @@ document.getElementById('contact-form').addEventListener('submit', async (event)
const button = event.target.querySelector('button[type="submit"]') const button = event.target.querySelector('button[type="submit"]')
button.disabled = true button.disabled = true
setMessage(status, '') setMessage(status, '')
if (session?.preview || !window.capsule) {
setMessage(status, 'Saved in preview only.', 'text-muted')
button.disabled = false
return
}
try { try {
await runApi(() => await runApi(() =>
window.capsule.contact({ window.capsule.contact({
@ -1119,12 +1225,17 @@ document.getElementById('bugreport-form').addEventListener('submit', async (even
const button = event.target.querySelector('button[type="submit"]') const button = event.target.querySelector('button[type="submit"]')
button.disabled = true button.disabled = true
setMessage(status, '') setMessage(status, '')
if (session?.preview || !window.capsule) {
setMessage(status, 'Saved in preview only.', 'text-muted')
button.disabled = false
return
}
try { try {
await runApi(() => await runApi(() =>
window.capsule.bugreport({ window.capsule.bugreport({
url: document.getElementById('bug-url').value.trim(), url: document.getElementById('bug-url').value.trim(),
ourl: document.getElementById('bug-ourl').value.trim(), ourl: document.getElementById('bug-ourl').value.trim(),
repeat: document.getElementById('bug-repeat').checked, repeat: document.getElementById('bug-repeat-yes').checked,
entry: document.getElementById('bug-entry').value entry: document.getElementById('bug-entry').value
}) })
) )
@ -1203,7 +1314,11 @@ document.getElementById('settings-avatar-file').addEventListener('change', async
}) })
document.getElementById('settings-dark').addEventListener('change', (event) => { document.getElementById('settings-dark').addEventListener('change', (event) => {
applyTheme(event.target.checked) setDarkMode(event.target.checked, true)
})
document.getElementById('dark-mode-toggle').addEventListener('change', (event) => {
setDarkMode(event.target.checked, true)
}) })
window.addEventListener('hashchange', route) window.addEventListener('hashchange', route)

View File

@ -74,14 +74,92 @@ body {
font-family: var(--ttp-font); font-family: var(--ttp-font);
} }
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
#main-content {
flex: 1 0 auto;
}
.context-main-bg { .context-main-bg {
background: var(--ttp-surface); background: var(--ttp-surface);
color: var(--ttp-text); color: var(--ttp-text);
} }
.context-main {
color: var(--ttp-text);
}
.context-main-border {
border-color: var(--ttp-border) !important;
}
footer {
flex-shrink: 0;
color: var(--ttp-text-muted);
font-size: 0.925rem;
}
footer a.context-main {
text-decoration: none;
}
/**
* Dark-mode footer switch.
*/
.material-switch {
position: relative;
display: inline-block;
width: 50px;
height: 25px;
}
.material-switch input {
opacity: 0;
width: 0;
height: 0;
}
.material-switch .label-default {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--switch-off-bg, var(--ttp-surface-muted));
border-radius: 25px;
transition: background-color 0.3s ease-in-out;
}
.material-switch .label-default::before {
content: '';
position: absolute;
height: 20px;
width: 20px;
border-radius: 50%;
background-color: var(--switch-slider-bg, #fff);
bottom: 2.5px;
left: 5px;
transition: transform 0.3s ease-in-out;
box-shadow: 0 2px 4px #00000033;
}
.material-switch input:checked + .label-default {
background-color: var(--switch-on-bg, var(--ttp-primary));
}
.material-switch input:checked + .label-default::before {
transform: translateX(25px);
}
header.text-bg-dark { header.text-bg-dark {
background-color: var(--ttp-chrome) !important; background-color: var(--ttp-chrome) !important;
box-shadow: 0 1px 0 rgba(63, 169, 245, 0.42), 0 10px 28px rgba(12, 25, 41, 0.18); box-shadow: 0 1px 0 rgba(63, 169, 245, 0.42), 0 10px 28px rgba(12, 25, 41, 0.18);
flex-shrink: 0;
} }
.capsule-header { .capsule-header {