From 1a2d85ad5d84ec8610925a1dc30d876169d8177d Mon Sep 17 00:00:00 2001 From: Craig Thacker Date: Sat, 8 Aug 2026 18:06:26 +0100 Subject: [PATCH] feat: add a GitHub link to the header and make the logo link home Two header links. The GitHub mark sits next to the theme toggle and opens the repository; the Libre DevOps logo now opens libredevops.org. The icon is the official GitHub mark, copied verbatim from primer/octicons icons/mark-github-16.svg. Inline SVG rather than a file or a remote icon, because the CSP is default-src 'self' with img-src 'self' data:, so inlining costs no extra request and needs no CSP change. fill=currentColor makes it follow the same colour as the controls beside it in both themes. Both links get target=_blank with rel=noopener noreferrer and an aria-label. The GitHub anchor joins the existing #theme-toggle selector lists rather than duplicating their declarations, so it picks up the same chrome and hover, with inline-flex centring the button gets for free. The logo anchor is inline-flex so wrapping the image does not disturb the header grid. Verified under jsdom: 30 checks, including that the shipped path matches the official octicons mark byte for byte, that the GitHub link is the theme toggle's next sibling, and that the logo anchor is still the direct grid child so the header layout is unchanged. --- css/styles.css | 1514 ++++++++++++++++++++++++------------------------ index.html | 40 +- 2 files changed, 805 insertions(+), 749 deletions(-) diff --git a/css/styles.css b/css/styles.css index 002c2af..5d67915 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,744 +1,770 @@ -/* ===== Design Tokens ===== */ -:root { - --bg-primary: #0d1117; - --bg-secondary: #161b22; - --bg-tertiary: #21262d; - - --text-primary: #f0f6fc; - --text-secondary: #b1bac4; - --text-tertiary: #7d8590; - - --border-color: #30363d; - - --brand-primary: #2b0a3d; - --brand-dark: #180422; - --brand-accent: #8b5cf6; - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35); - --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); - - --radius-sm: 10px; - --radius-md: 16px; - --radius-lg: 22px; - - --transition: 0.2s ease; -} - -/* ===== Reset ===== */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -/* ===== Base ===== */ -html { - scroll-behavior: smooth; -} - -body { - font-family: - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - Helvetica, - Arial, - sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - min-height: 100vh; - line-height: 1.6; - /* Guard against any child overflowing horizontally and pulling the page - wider than the viewport, which leaves a blank strip beside the header. */ - overflow-x: hidden; -} - -/* ===== Header ===== */ -header { - background: - radial-gradient(circle at top left, rgba(139, 92, 246, 0.15), transparent 45%), - linear-gradient(135deg, var(--brand-primary), var(--brand-dark)); - color: white; - padding: 1.5rem 2rem 1rem; - box-shadow: var(--shadow-lg); -} - -.header-content { - max-width: 1440px; - margin: 0 auto; -} - -.logo { - display: grid; - grid-template-columns: auto 1fr auto; - align-items: center; - gap: 1rem; - width: 100%; -} - -.site-logo { - width: 110px; - height: 110px; - object-fit: contain; - flex-shrink: 0; -} - -[data-theme="dark"] .site-logo { - content: url("../security-libre-devops-white.png"); -} - -[data-theme="light"] .site-logo { - content: url("../security-libre-devops-white.png"); -} - -.logo-text { - text-align: center; -} - -.logo h1 { - font-size: 2rem; - font-weight: 800; - line-height: 1.15; -} - -.logo .subtitle { - margin-top: 0.35rem; - font-size: 1rem; - color: rgba(255, 255, 255, 0.88); -} - -.logo-spacer { - width: 110px; -} - -/* ===== Header Actions ===== */ -.header-actions { - display: flex; - justify-content: center; - gap: 0.85rem; - flex-wrap: wrap; - margin-top: 1.25rem; -} - -/* ===== Search ===== */ -.search-box { - width: 100%; - max-width: 760px; - position: relative; -} - -.search-box .search-icon { - position: absolute; - left: 1rem; - top: 50%; - transform: translateY(-50%); - opacity: 0.8; -} - -.search-box input { - width: 100%; - padding: 0.95rem 1rem 0.95rem 3rem; - border: none; - border-radius: 999px; - background: rgba(255, 255, 255, 0.14); - color: white; - font-size: 1rem; - outline: none; - backdrop-filter: blur(12px); -} - -.search-box input::placeholder { - color: rgba(255, 255, 255, 0.72); -} - -/* ===== Buttons ===== */ -#bookmarks-toggle, -#theme-toggle, -.rss-button { - border: 1px solid rgba(255, 255, 255, 0.18); - background: rgba(255, 255, 255, 0.08); - color: white; - cursor: pointer; - transition: all var(--transition); - backdrop-filter: blur(10px); -} - -#bookmarks-toggle, -.rss-button { - padding: 0.75rem 1.2rem; - border-radius: 999px; - font-size: 0.95rem; - text-decoration: none; -} - -#theme-toggle { - width: 52px; - height: 52px; - border-radius: 50%; - font-size: 1.4rem; -} - -#bookmarks-toggle:hover, -#theme-toggle:hover, -.rss-button:hover { - background: rgba(255, 255, 255, 0.16); -} - -#bookmarks-toggle.active { - background: rgba(139, 92, 246, 0.35); - border-color: var(--brand-accent); -} - -/* ===== Stats ===== */ -.stats-bar { - max-width: 1440px; - margin: 1rem auto 0; - display: flex; - justify-content: center; - gap: 1.25rem; - flex-wrap: wrap; - font-size: 0.95rem; - color: rgba(255, 255, 255, 0.82); -} - -/* ===== Filters ===== */ -.filters { - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - padding: 0.8rem 1.5rem; -} - -.filter-section-title { - max-width: 1440px; - margin: 0 auto 0.5rem; - font-size: 0.82rem; - font-weight: 700; - text-transform: uppercase; - color: var(--text-secondary); -} - -.filter-scroll { - max-width: 1440px; - margin: 0 auto; - overflow-x: auto; - overflow-y: hidden; - -webkit-overflow-scrolling: touch; - scrollbar-width: thin; -} - -.filter-pills { - display: flex; - gap: 0.5rem; - flex-wrap: nowrap; - padding-bottom: 0.25rem; -} - -.pill { - border: 1px solid var(--border-color); - background: var(--bg-primary); - color: var(--text-secondary); - padding: 0.5rem 0.9rem; - border-radius: 999px; - cursor: pointer; - white-space: nowrap; - font-size: 0.8rem; - flex-shrink: 0; - transition: all var(--transition); -} - -.pill:hover { - border-color: var(--brand-accent); -} - -.pill.active { - background: var(--brand-primary); - border-color: var(--brand-primary); - color: white; -} - -.pill .count { - margin-left: 0.4rem; - opacity: 0.85; -} - -/* ===== Main ===== */ -main { - max-width: 1440px; - margin: 0 auto; - padding: 1.5rem; -} - -/* ===== Controls ===== */ -.controls { - display: flex; - justify-content: space-between; - align-items: center; - gap: 1rem; - flex-wrap: wrap; - margin-bottom: 1.25rem; -} - -.controls-right { - display: flex; - gap: 0.75rem; -} - -#date-filter, -#sort-by { - padding: 0.75rem 1rem; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - color: var(--text-primary); - border-radius: 12px; - min-width: 160px; -} - -/* ===== Loading ===== */ -.loading { - display: none; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - padding: 4rem 2rem; - color: var(--text-secondary); - font-size: 1rem; -} - -.loading.visible { - display: flex; -} - -.loading-spinner { - width: 40px; - height: 40px; - border: 3px solid var(--border-color); - border-top-color: var(--brand-accent); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { - transform: rotate(360deg); - } -} - -/* ===== No Results ===== */ -.no-results { - display: none; - text-align: center; - padding: 4rem 2rem; - color: var(--text-secondary); -} - -.no-results.visible { - display: block; -} - -/* ===== Grid ===== */ -.articles-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); - gap: 1rem; -} - -/* ===== Cards ===== */ -.article-card { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - padding: 1.2rem; - transition: all var(--transition); - /* Column flexbox so the Read More button can be pushed to the bottom of the - card with margin-top:auto, instead of floating wherever the summary happens - to end. */ - display: flex; - flex-direction: column; - /* Allow the card to shrink below its content's intrinsic width inside the - grid track; without this a long unbreakable token (URL, hash) widens the - track and overflows the page on Chrome mobile. */ - min-width: 0; -} - -.article-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); - border-color: rgba(139, 92, 246, 0.3); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 0.5rem; -} - -.blog-tag { - padding: 0.3rem 0.75rem; - border-radius: 999px; - font-size: 0.72rem; - font-weight: 700; -} - -.bookmark-btn { - background: none; - border: none; - font-size: 1.2rem; - cursor: pointer; - color: var(--text-secondary); - flex-shrink: 0; - align-self: flex-start; -} - -.bookmark-btn.bookmarked { - color: #f59e0b; -} - -.article-title { - font-size: 1.05rem; - font-weight: 700; - margin: 0.8rem 0 0.5rem; - overflow-wrap: anywhere; -} - -.article-title a { - color: var(--text-primary); - text-decoration: none; - transition: color var(--transition); -} - -.article-title a:hover { - color: var(--brand-accent); -} - -.article-meta { - display: flex; - gap: 0.5rem 1rem; - flex-wrap: wrap; - font-size: 0.8rem; - color: var(--text-tertiary); - margin-bottom: 0.75rem; - line-height: 1.4; - overflow-wrap: anywhere; -} - -.article-summary { - color: var(--text-secondary); - font-size: 0.92rem; - line-height: 1.55; - overflow-wrap: anywhere; - /* Explicit, so a full card (where margin-top:auto resolves to zero) still - keeps the button clear of the text. */ - margin-bottom: 1rem; -} - -/* ===== Toast ===== */ -.toast { - position: fixed; - bottom: 2rem; - left: 50%; - transform: translateX(-50%) translateY(1rem); - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - color: var(--text-primary); - padding: 0.75rem 1.5rem; - border-radius: 999px; - opacity: 0; - pointer-events: none; - transition: all 0.25s ease; - z-index: 9999; -} - -.toast.visible { - opacity: 1; - transform: translateX(-50%) translateY(0); -} - -/* ===== Mobile ===== */ -@media (max-width: 768px) { - header { - padding: 1rem 1rem 1.25rem; - } - - .logo { - grid-template-columns: 1fr; - justify-items: center; - text-align: center; - gap: 0.75rem; - } - - .logo-spacer { - display: none; - } - - .site-logo { - width: 60px; - height: 60px; - } - - .logo h1 { - font-size: 2rem; - text-align: center; - } - - .logo .subtitle { - font-size: 0.95rem; - max-width: 320px; - } - - .header-actions { - margin-top: 1rem; - flex-direction: column; - align-items: center; - } - - .search-box { - width: 100%; - } - - .controls { - flex-direction: column; - align-items: stretch; - } - - .controls-right { - width: 100%; - } - - .controls-right select { - flex: 1; - min-width: unset; - } - - .articles-grid { - grid-template-columns: 1fr; - } - - .stats-bar { - gap: 0.75rem; - font-size: 0.85rem; - } -} - -@media (max-width: 480px) { - .logo h1 { - font-size: 1.75rem; - } - - .logo .subtitle { - font-size: 0.88rem; - } - - .search-box input { - font-size: 0.95rem; - padding: 0.85rem 1rem 0.85rem 2.75rem; - } - - .filter-pills { - gap: 0.4rem; - } - - .pill { - padding: 0.5rem 0.85rem; - font-size: 0.8rem; - } - - #bookmarks-toggle, - .rss-button { - width: 100%; - max-width: 420px; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - } - - #theme-toggle { - align-self: center; - } - - .article-meta { - flex-direction: column; - gap: 0.35rem; - } -} - -/* ===== Light Theme ===== */ -[data-theme="light"] { - --bg-primary: #f6f8fa; - --bg-secondary: #ffffff; - --bg-tertiary: #eaeef2; - - --text-primary: #1c2128; - --text-secondary: #424a53; - --text-tertiary: #656d76; - - --border-color: #d0d7de; -} - -[data-theme="light"] .pill { - background: var(--bg-secondary); - color: var(--text-secondary); -} - -[data-theme="light"] .pill.active { - background: var(--brand-primary); - border-color: var(--brand-primary); - color: white; -} -/* --------------------------------------------------------------------------- - Full post viewer - - Only articles carrying a complete body (currently Microsoft Message Center - posts) get an expand button, because a Message Center permalink only opens - for someone with tenant admin access. Showing the post inline is the point. - Uses the native , so focus trapping and Escape come from the - platform. Both themes are driven by the existing custom properties. ---------------------------------------------------------------------------- */ - -.expand-btn { - /* Pinned to the bottom left of the card. Grid tracks already stretch cards in - a row to equal height, so with the card as a column flexbox `margin-top: - auto` eats the leftover space and every button lands on the same baseline - regardless of how long its summary runs. align-self stops it stretching to - the full card width. */ - margin-top: auto; - align-self: flex-start; - padding: 0.5rem 1rem; - font-size: 0.85rem; - font-weight: 600; - font-family: inherit; - color: var(--text-primary); - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - cursor: pointer; - transition: var(--transition); -} - -.expand-btn:hover { - border-color: var(--brand-accent); - color: var(--brand-accent); -} - -.expand-btn:focus-visible { - outline: 2px solid var(--brand-accent); - outline-offset: 2px; -} - -.article-dialog { - padding: 0; - border: none; - background: transparent; - max-width: min(760px, 92vw); - max-height: 88vh; - overflow: visible; -} - -.article-dialog::backdrop { - background: rgba(0, 0, 0, 0.65); - backdrop-filter: blur(2px); -} - -.dialog-panel { - position: relative; - max-height: 88vh; - overflow-y: auto; - padding: 2rem; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); -} - -.dialog-close { - position: sticky; - top: 0; - float: right; - width: 2rem; - height: 2rem; - font-size: 1.4rem; - line-height: 1; - font-family: inherit; - color: var(--text-secondary); - background: var(--bg-tertiary); - border: 1px solid var(--border-color); - border-radius: 50%; - cursor: pointer; - transition: var(--transition); -} - -.dialog-close:hover { - color: var(--text-primary); - border-color: var(--brand-accent); -} - -.dialog-tags { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; - margin-bottom: 1rem; -} - -.dialog-title { - margin: 0 0 0.75rem; - font-size: 1.4rem; - line-height: 1.35; - color: var(--text-primary); -} - -.dialog-text { - margin-top: 1.25rem; - color: var(--text-secondary); - line-height: 1.7; -} - -.dialog-text p { - margin: 0 0 1rem; -} - -.dialog-subheading { - margin: 1.5rem 0 0.75rem; - font-size: 1rem; - color: var(--text-primary); -} - -.dialog-links { - margin: 0; - padding-left: 1.2rem; - color: var(--text-secondary); -} - -.dialog-links li { - margin-bottom: 0.4rem; -} - -.dialog-links a, -.dialog-source-link { - color: var(--brand-accent); - overflow-wrap: anywhere; -} - -.dialog-source-link { - display: inline-block; - margin-top: 1.5rem; - padding-top: 1.25rem; - border-top: 1px solid var(--border-color); - font-size: 0.9rem; - font-weight: 600; -} - -@media (max-width: 600px) { - .dialog-panel { - padding: 1.25rem; - } - - .dialog-title { - font-size: 1.2rem; - } -} +/* ===== Design Tokens ===== */ +:root { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + + --text-primary: #f0f6fc; + --text-secondary: #b1bac4; + --text-tertiary: #7d8590; + + --border-color: #30363d; + + --brand-primary: #2b0a3d; + --brand-dark: #180422; + --brand-accent: #8b5cf6; + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5); + + --radius-sm: 10px; + --radius-md: 16px; + --radius-lg: 22px; + + --transition: 0.2s ease; +} + +/* ===== Reset ===== */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ===== Base ===== */ +html { + scroll-behavior: smooth; +} + +body { + font-family: + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Helvetica, + Arial, + sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + line-height: 1.6; + /* Guard against any child overflowing horizontally and pulling the page + wider than the viewport, which leaves a blank strip beside the header. */ + overflow-x: hidden; +} + +/* ===== Header ===== */ +header { + background: + radial-gradient(circle at top left, rgba(139, 92, 246, 0.15), transparent 45%), + linear-gradient(135deg, var(--brand-primary), var(--brand-dark)); + color: white; + padding: 1.5rem 2rem 1rem; + box-shadow: var(--shadow-lg); +} + +.header-content { + max-width: 1440px; + margin: 0 auto; +} + +.logo { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 1rem; + width: 100%; +} + +.site-logo { + width: 110px; + height: 110px; + object-fit: contain; + flex-shrink: 0; +} + +[data-theme="dark"] .site-logo { + content: url("../security-libre-devops-white.png"); +} + +[data-theme="light"] .site-logo { + content: url("../security-libre-devops-white.png"); +} + +.logo-text { + text-align: center; +} + +.logo h1 { + font-size: 2rem; + font-weight: 800; + line-height: 1.15; +} + +.logo .subtitle { + margin-top: 0.35rem; + font-size: 1rem; + color: rgba(255, 255, 255, 0.88); +} + +.logo-spacer { + width: 110px; +} + +/* ===== Header Actions ===== */ +.header-actions { + display: flex; + justify-content: center; + gap: 0.85rem; + flex-wrap: wrap; + margin-top: 1.25rem; +} + +/* ===== Search ===== */ +.search-box { + width: 100%; + max-width: 760px; + position: relative; +} + +.search-box .search-icon { + position: absolute; + left: 1rem; + top: 50%; + transform: translateY(-50%); + opacity: 0.8; +} + +.search-box input { + width: 100%; + padding: 0.95rem 1rem 0.95rem 3rem; + border: none; + border-radius: 999px; + background: rgba(255, 255, 255, 0.14); + color: white; + font-size: 1rem; + outline: none; + backdrop-filter: blur(12px); +} + +.search-box input::placeholder { + color: rgba(255, 255, 255, 0.72); +} + +/* ===== Buttons ===== */ +#bookmarks-toggle, +#theme-toggle, +#github-link, +.rss-button { + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.08); + color: white; + cursor: pointer; + transition: all var(--transition); + backdrop-filter: blur(10px); +} + +#bookmarks-toggle, +.rss-button { + padding: 0.75rem 1.2rem; + border-radius: 999px; + font-size: 0.95rem; + text-decoration: none; +} + +#theme-toggle, +#github-link { + width: 52px; + height: 52px; + border-radius: 50%; + font-size: 1.4rem; +} + +/* The anchor needs explicit centring that the button gets for free. */ +#github-link { + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; +} + +/* The logo is a grid item; the anchor wrapping it must not change that layout, + only make the image clickable. */ +.logo-link { + display: inline-flex; + align-items: center; + text-decoration: none; + border-radius: var(--radius-sm); +} + +.logo-link:focus-visible, +#github-link:focus-visible { + outline: 2px solid white; + outline-offset: 3px; +} + +#bookmarks-toggle:hover, +#theme-toggle:hover, +#github-link:hover, +.rss-button:hover { + background: rgba(255, 255, 255, 0.16); +} + +#bookmarks-toggle.active { + background: rgba(139, 92, 246, 0.35); + border-color: var(--brand-accent); +} + +/* ===== Stats ===== */ +.stats-bar { + max-width: 1440px; + margin: 1rem auto 0; + display: flex; + justify-content: center; + gap: 1.25rem; + flex-wrap: wrap; + font-size: 0.95rem; + color: rgba(255, 255, 255, 0.82); +} + +/* ===== Filters ===== */ +.filters { + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + padding: 0.8rem 1.5rem; +} + +.filter-section-title { + max-width: 1440px; + margin: 0 auto 0.5rem; + font-size: 0.82rem; + font-weight: 700; + text-transform: uppercase; + color: var(--text-secondary); +} + +.filter-scroll { + max-width: 1440px; + margin: 0 auto; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; +} + +.filter-pills { + display: flex; + gap: 0.5rem; + flex-wrap: nowrap; + padding-bottom: 0.25rem; +} + +.pill { + border: 1px solid var(--border-color); + background: var(--bg-primary); + color: var(--text-secondary); + padding: 0.5rem 0.9rem; + border-radius: 999px; + cursor: pointer; + white-space: nowrap; + font-size: 0.8rem; + flex-shrink: 0; + transition: all var(--transition); +} + +.pill:hover { + border-color: var(--brand-accent); +} + +.pill.active { + background: var(--brand-primary); + border-color: var(--brand-primary); + color: white; +} + +.pill .count { + margin-left: 0.4rem; + opacity: 0.85; +} + +/* ===== Main ===== */ +main { + max-width: 1440px; + margin: 0 auto; + padding: 1.5rem; +} + +/* ===== Controls ===== */ +.controls { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1.25rem; +} + +.controls-right { + display: flex; + gap: 0.75rem; +} + +#date-filter, +#sort-by { + padding: 0.75rem 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: 12px; + min-width: 160px; +} + +/* ===== Loading ===== */ +.loading { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 4rem 2rem; + color: var(--text-secondary); + font-size: 1rem; +} + +.loading.visible { + display: flex; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border-color); + border-top-color: var(--brand-accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ===== No Results ===== */ +.no-results { + display: none; + text-align: center; + padding: 4rem 2rem; + color: var(--text-secondary); +} + +.no-results.visible { + display: block; +} + +/* ===== Grid ===== */ +.articles-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: 1rem; +} + +/* ===== Cards ===== */ +.article-card { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 1.2rem; + transition: all var(--transition); + /* Column flexbox so the Read More button can be pushed to the bottom of the + card with margin-top:auto, instead of floating wherever the summary happens + to end. */ + display: flex; + flex-direction: column; + /* Allow the card to shrink below its content's intrinsic width inside the + grid track; without this a long unbreakable token (URL, hash) widens the + track and overflows the page on Chrome mobile. */ + min-width: 0; +} + +.article-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: rgba(139, 92, 246, 0.3); +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 0.5rem; +} + +.blog-tag { + padding: 0.3rem 0.75rem; + border-radius: 999px; + font-size: 0.72rem; + font-weight: 700; +} + +.bookmark-btn { + background: none; + border: none; + font-size: 1.2rem; + cursor: pointer; + color: var(--text-secondary); + flex-shrink: 0; + align-self: flex-start; +} + +.bookmark-btn.bookmarked { + color: #f59e0b; +} + +.article-title { + font-size: 1.05rem; + font-weight: 700; + margin: 0.8rem 0 0.5rem; + overflow-wrap: anywhere; +} + +.article-title a { + color: var(--text-primary); + text-decoration: none; + transition: color var(--transition); +} + +.article-title a:hover { + color: var(--brand-accent); +} + +.article-meta { + display: flex; + gap: 0.5rem 1rem; + flex-wrap: wrap; + font-size: 0.8rem; + color: var(--text-tertiary); + margin-bottom: 0.75rem; + line-height: 1.4; + overflow-wrap: anywhere; +} + +.article-summary { + color: var(--text-secondary); + font-size: 0.92rem; + line-height: 1.55; + overflow-wrap: anywhere; + /* Explicit, so a full card (where margin-top:auto resolves to zero) still + keeps the button clear of the text. */ + margin-bottom: 1rem; +} + +/* ===== Toast ===== */ +.toast { + position: fixed; + bottom: 2rem; + left: 50%; + transform: translateX(-50%) translateY(1rem); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 0.75rem 1.5rem; + border-radius: 999px; + opacity: 0; + pointer-events: none; + transition: all 0.25s ease; + z-index: 9999; +} + +.toast.visible { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* ===== Mobile ===== */ +@media (max-width: 768px) { + header { + padding: 1rem 1rem 1.25rem; + } + + .logo { + grid-template-columns: 1fr; + justify-items: center; + text-align: center; + gap: 0.75rem; + } + + .logo-spacer { + display: none; + } + + .site-logo { + width: 60px; + height: 60px; + } + + .logo h1 { + font-size: 2rem; + text-align: center; + } + + .logo .subtitle { + font-size: 0.95rem; + max-width: 320px; + } + + .header-actions { + margin-top: 1rem; + flex-direction: column; + align-items: center; + } + + .search-box { + width: 100%; + } + + .controls { + flex-direction: column; + align-items: stretch; + } + + .controls-right { + width: 100%; + } + + .controls-right select { + flex: 1; + min-width: unset; + } + + .articles-grid { + grid-template-columns: 1fr; + } + + .stats-bar { + gap: 0.75rem; + font-size: 0.85rem; + } +} + +@media (max-width: 480px) { + .logo h1 { + font-size: 1.75rem; + } + + .logo .subtitle { + font-size: 0.88rem; + } + + .search-box input { + font-size: 0.95rem; + padding: 0.85rem 1rem 0.85rem 2.75rem; + } + + .filter-pills { + gap: 0.4rem; + } + + .pill { + padding: 0.5rem 0.85rem; + font-size: 0.8rem; + } + + #bookmarks-toggle, + .rss-button { + width: 100%; + max-width: 420px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + } + + #theme-toggle { + align-self: center; + } + + .article-meta { + flex-direction: column; + gap: 0.35rem; + } +} + +/* ===== Light Theme ===== */ +[data-theme="light"] { + --bg-primary: #f6f8fa; + --bg-secondary: #ffffff; + --bg-tertiary: #eaeef2; + + --text-primary: #1c2128; + --text-secondary: #424a53; + --text-tertiary: #656d76; + + --border-color: #d0d7de; +} + +[data-theme="light"] .pill { + background: var(--bg-secondary); + color: var(--text-secondary); +} + +[data-theme="light"] .pill.active { + background: var(--brand-primary); + border-color: var(--brand-primary); + color: white; +} +/* --------------------------------------------------------------------------- + Full post viewer + + Only articles carrying a complete body (currently Microsoft Message Center + posts) get an expand button, because a Message Center permalink only opens + for someone with tenant admin access. Showing the post inline is the point. + Uses the native , so focus trapping and Escape come from the + platform. Both themes are driven by the existing custom properties. +--------------------------------------------------------------------------- */ + +.expand-btn { + /* Pinned to the bottom left of the card. Grid tracks already stretch cards in + a row to equal height, so with the card as a column flexbox `margin-top: + auto` eats the leftover space and every button lands on the same baseline + regardless of how long its summary runs. align-self stops it stretching to + the full card width. */ + margin-top: auto; + align-self: flex-start; + padding: 0.5rem 1rem; + font-size: 0.85rem; + font-weight: 600; + font-family: inherit; + color: var(--text-primary); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + cursor: pointer; + transition: var(--transition); +} + +.expand-btn:hover { + border-color: var(--brand-accent); + color: var(--brand-accent); +} + +.expand-btn:focus-visible { + outline: 2px solid var(--brand-accent); + outline-offset: 2px; +} + +.article-dialog { + padding: 0; + border: none; + background: transparent; + max-width: min(760px, 92vw); + max-height: 88vh; + overflow: visible; +} + +.article-dialog::backdrop { + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(2px); +} + +.dialog-panel { + position: relative; + max-height: 88vh; + overflow-y: auto; + padding: 2rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); +} + +.dialog-close { + position: sticky; + top: 0; + float: right; + width: 2rem; + height: 2rem; + font-size: 1.4rem; + line-height: 1; + font-family: inherit; + color: var(--text-secondary); + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 50%; + cursor: pointer; + transition: var(--transition); +} + +.dialog-close:hover { + color: var(--text-primary); + border-color: var(--brand-accent); +} + +.dialog-tags { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +.dialog-title { + margin: 0 0 0.75rem; + font-size: 1.4rem; + line-height: 1.35; + color: var(--text-primary); +} + +.dialog-text { + margin-top: 1.25rem; + color: var(--text-secondary); + line-height: 1.7; +} + +.dialog-text p { + margin: 0 0 1rem; +} + +.dialog-subheading { + margin: 1.5rem 0 0.75rem; + font-size: 1rem; + color: var(--text-primary); +} + +.dialog-links { + margin: 0; + padding-left: 1.2rem; + color: var(--text-secondary); +} + +.dialog-links li { + margin-bottom: 0.4rem; +} + +.dialog-links a, +.dialog-source-link { + color: var(--brand-accent); + overflow-wrap: anywhere; +} + +.dialog-source-link { + display: inline-block; + margin-top: 1.5rem; + padding-top: 1.25rem; + border-top: 1px solid var(--border-color); + font-size: 0.9rem; + font-weight: 600; +} + +@media (max-width: 600px) { + .dialog-panel { + padding: 1.25rem; + } + + .dialog-title { + font-size: 1.2rem; + } +} diff --git a/index.html b/index.html index 6cd0b79..15d744c 100644 --- a/index.html +++ b/index.html @@ -69,11 +69,19 @@