/* Photography section — Phase 1 minimal styles. * * Phase 2 will introduce the masonry / grid / chronological mode toggle, * darkroom lightbox treatment, and richer card hover states. For now the * goal is structural correctness and clean defaults so a single photo * page reads end-to-end. * * Style scope: applied via $if(photography)$ gate in templates/partials/head.html, * so these rules only load on photography pages. */ /* --------------------------------------------------------------------------- * Single-photo page * --------------------------------------------------------------------------- */ .photo-entry { max-width: 60rem; margin: 0 auto; } .photo-header { margin-bottom: 1.5rem; } .photo-figure { margin: 1rem 0 1.25rem; text-align: center; } .photo-figure img { max-width: 100%; height: auto; cursor: zoom-in; } .photo-caption { margin-top: 0.5rem; font-style: italic; color: var(--text-muted); font-size: 0.95em; } .photo-captured { color: var(--text-muted); font-size: 0.95em; } .photo-location { font-style: italic; } /* Color palette strip (Phase 1 stub; populated by Phase 3 auto-extraction * once tools/extract-palette.py lands). Renders only when frontmatter has * a non-empty palette: list. */ .photo-palette { display: flex; gap: 2px; margin: 1rem 0 1.5rem; height: 0.75rem; } .photo-swatch { flex: 1; border-radius: 1px; } /* Per-photo metadata block — camera, lens, exposure, etc. Two-column * description list, definition-list semantics for accessibility. */ .photo-meta { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; margin: 1.25rem 0; padding: 0.75rem 1rem; border-left: 2px solid var(--border-muted); background: var(--bg-muted); font-size: 0.9em; } .photo-meta dt { font-weight: 600; color: var(--text-muted); text-transform: lowercase; letter-spacing: 0.02em; } .photo-meta dd { margin: 0; } .photo-license { color: inherit; } .photo-external-links { margin: 0.75rem 0 1.25rem; font-size: 0.9em; color: var(--text-muted); } .photo-external-links .meta-label { margin-right: 0.5rem; text-transform: lowercase; letter-spacing: 0.02em; } .photo-external-link { color: var(--text); } /* --------------------------------------------------------------------------- * Section landing — /photography/ * --------------------------------------------------------------------------- */ .photography-intro { max-width: 36rem; margin: 0 auto 2rem; color: var(--text-muted); } .photography-empty { text-align: center; color: var(--text-faint); font-style: italic; margin: 4rem auto; } /* --------------------------------------------------------------------------- * Mode toggle UI * --------------------------------------------------------------------------- */ .photography-header { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 1.5rem; } .photography-controls { display: flex; align-items: center; gap: 0.5rem; } .photography-mode-toggle { display: inline-flex; border: 1px solid var(--border); border-radius: 2px; overflow: hidden; font-size: 0.85em; } .photography-mode-toggle .mode-btn { padding: 0.35rem 0.75rem; background: transparent; border: none; border-right: 1px solid var(--border); color: var(--text-muted); cursor: pointer; font: inherit; font-size: inherit; letter-spacing: 0.02em; transition: background 120ms ease, color 120ms ease; } .photography-mode-toggle .mode-btn:last-child { border-right: none; } .photography-mode-toggle .mode-btn:hover { color: var(--text); } .photography-mode-toggle .mode-btn.is-active { background: var(--text); color: var(--bg); } /* --------------------------------------------------------------------------- * Grid container — base + per-mode rules. * * The same .photography-grid markup feeds three layout strategies; the * data-photography-mode attribute (set by photography-modes.js) keys all * per-mode rules. JS also sets inline grid-row spans on each card in * masonry mode; clearing the attribute clears the inline style. * --------------------------------------------------------------------------- */ .photography-grid { list-style: none; padding: 0; margin: 0; display: grid; } /* Masonry — variable-height cells respecting native aspect ratios. * grid-auto-rows + JS-applied grid-row spans synthesize true masonry. */ .photography-grid[data-photography-mode="masonry"] { grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); grid-auto-rows: 8px; /* must match ROW_UNIT in photography-modes.js */ gap: 8px; /* must match ROW_GAP in photography-modes.js */ align-items: stretch; } .photography-grid[data-photography-mode="masonry"] .photo-card-img { width: 100%; height: auto; display: block; } /* Uniform grid — square cells, object-fit: cover. Compare side-by-side. */ .photography-grid[data-photography-mode="grid"] { grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); gap: 0.75rem; } .photography-grid[data-photography-mode="grid"] .photo-card-img { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; display: block; background: var(--bg-muted); } /* Chronological — single column, large, contemplative. */ .photography-grid[data-photography-mode="chronological"] { grid-template-columns: minmax(0, 48rem); justify-content: center; gap: 3rem; } .photography-grid[data-photography-mode="chronological"] .photo-card-img { width: 100%; height: auto; display: block; } .photography-grid[data-photography-mode="chronological"] .photo-card-meta { margin-top: 0.75rem; font-size: 1em; } /* --------------------------------------------------------------------------- * Card chrome — shared across all three modes * --------------------------------------------------------------------------- */ .photo-card { margin: 0; } .photo-card-link { display: block; text-decoration: none; color: inherit; overflow: hidden; } .photo-card-img { background: var(--bg-muted); } .photo-card-meta { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin-top: 0.4rem; font-size: 0.9em; } .photo-card-title { font-weight: 500; } .photo-card-date { color: var(--text-muted); font-variant-numeric: oldstyle-nums; flex-shrink: 0; } /* --------------------------------------------------------------------------- * Darkroom lightbox — photography pages only * * Augments the existing lightbox (static/css/images.css) when the JS * detects body[data-page-type="photography"] and adds the .darkroom * class to the overlay. Non-photography lightbox styling is unaffected. * --------------------------------------------------------------------------- */ .lightbox-overlay.darkroom { background: #000; } .lightbox-overlay.darkroom .lightbox-vignette { position: absolute; inset: 0; pointer-events: none; background: radial-gradient( ellipse at center, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0.55) 100% ); transition: opacity 200ms ease; } .lightbox-overlay:not(.darkroom) .lightbox-vignette, .lightbox-overlay:not(.darkroom) .lightbox-info-toggle, .lightbox-overlay:not(.darkroom) .lightbox-info-panel { display: none; } .lightbox-overlay.darkroom .lightbox-caption { color: rgba(255, 255, 255, 0.78); font-style: italic; font-size: 0.95em; max-width: 48rem; margin: 1rem auto 0; text-align: center; } .lightbox-overlay.darkroom .lightbox-close { color: rgba(255, 255, 255, 0.8); } /* "i" toggle button — top-right corner alongside the close button. */ .lightbox-info-toggle { position: absolute; top: 0.75rem; right: 3.5rem; z-index: 2; width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center; background: transparent; border: 1px solid rgba(255, 255, 255, 0.4); border-radius: 50%; color: rgba(255, 255, 255, 0.8); font-size: 1rem; cursor: pointer; transition: background 120ms ease, border-color 120ms ease; } .lightbox-info-toggle:hover, .lightbox-info-toggle[aria-pressed="true"] { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.7); } /* Info panel — slides in from below; muted serif for the EXIF strip. */ .lightbox-info-panel { position: absolute; bottom: 0; left: 50%; transform: translate(-50%, 100%); max-width: 48rem; width: calc(100% - 2rem); padding: 1rem 1.25rem; background: rgba(0, 0, 0, 0.85); color: rgba(255, 255, 255, 0.85); border-top: 1px solid rgba(255, 255, 255, 0.12); transition: transform 220ms ease; pointer-events: none; } .lightbox-overlay.is-info-visible .lightbox-info-panel { transform: translate(-50%, 0); pointer-events: auto; } .lightbox-info-panel dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1.25rem; margin: 0; font-size: 0.9em; } .lightbox-info-panel dt { font-weight: 500; color: rgba(255, 255, 255, 0.55); text-transform: lowercase; letter-spacing: 0.04em; } .lightbox-info-panel dd { margin: 0; color: rgba(255, 255, 255, 0.92); } /* --------------------------------------------------------------------------- * Map page — /photography/map/ * * The viewport is sized in viewport-relative units so the map fills * a useful portion of the screen without dominating it; the * surrounding page chrome (header, attribution paragraph, footer) * stays visible at typical desktop heights. * * Leaflet's own stylesheet handles tile / control / popup styling; * these rules cover only the page-level shell, the tooltip content * we render via tooltipHtml(), and the no-pin / error fallback states. * --------------------------------------------------------------------------- */ .photography-map { height: 70vh; min-height: 32rem; margin: 1.5rem 0 1rem; border: 1px solid var(--border); background: var(--bg-muted); } .photography-map--empty, .photography-map--error { display: flex; align-items: center; justify-content: center; text-align: center; padding: 2rem; } .photography-map-empty, .photography-map-error, .photography-map-fallback { max-width: 32rem; margin: 0 auto; color: var(--text-muted); font-style: italic; } .photography-map-note { margin: 0.5rem 0 2rem; color: var(--text-faint); font-size: 0.85em; max-width: 48rem; } .photography-map-note code { background: var(--bg-muted); padding: 0 0.3em; border-radius: 2px; font-size: 0.9em; } /* Tooltip content — rendered by photography-map.js inside Leaflet's * default tooltip wrapper. The wrapper class .photography-map-tooltip-wrap * lets us tighten Leaflet's default padding without bleeding into the * other Leaflet popups. */ .leaflet-tooltip.photography-map-tooltip-wrap { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 2px; padding: 0; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); font-family: inherit; } /* Leaflet draws tooltip arrows via ::before; recolor to match. */ .leaflet-tooltip.photography-map-tooltip-wrap::before { border-top-color: var(--border); } .photography-map-tooltip { width: 14rem; max-width: 16rem; } .photography-map-tooltip-img { display: block; width: 100%; height: 8rem; object-fit: cover; margin: 0; border-bottom: 1px solid var(--border); } .photography-map-tooltip-title { padding: 0.4rem 0.6rem 0.1rem; font-weight: 500; line-height: 1.2; } .photography-map-tooltip-date { padding: 0 0.6rem 0.4rem; color: var(--text-muted); font-size: 0.85em; font-variant-numeric: oldstyle-nums; } /* --------------------------------------------------------------------------- * By-year index — /photography/by-year/ * * A simple narrow column of years, each link a small card with the * year (large oldstyle figures) and a count (muted small caps). * --------------------------------------------------------------------------- */ .photography-header--narrow { flex-direction: column; align-items: flex-start; gap: 0.25rem; } .photography-by-year-back { margin: 0 0 1rem; color: var(--text-muted); font-size: 0.9em; } .photography-by-year-list { list-style: none; padding: 0; margin: 1rem 0 2rem; max-width: 24rem; } .photography-by-year-item { margin: 0; } .photography-by-year-link { display: flex; justify-content: space-between; align-items: baseline; padding: 0.5rem 0; border-bottom: 1px solid var(--border-muted); text-decoration: none; color: inherit; transition: color 120ms ease; } .photography-by-year-link:hover { color: var(--text); } .photography-by-year-year { font-size: 1.4em; font-variant-numeric: oldstyle-nums; } .photography-by-year-count { color: var(--text-muted); font-size: 0.85em; text-transform: lowercase; letter-spacing: 0.04em; } .photography-by-year-count::before { content: ""; display: inline-block; width: 0; } .photography-by-year-count::after { content: " photographs"; } /* --------------------------------------------------------------------------- * Contact sheet — /photography/contact-sheet/ * * Frame-numbered grid in the visual register of an analog contact * print: small uniform thumbnails, thin white border per frame, * frame number in the corner via a CSS counter. The dark page * backdrop is a soft "film-room" gray rather than full black so * the white frames don't ring against the surrounding page chrome * too hard. * --------------------------------------------------------------------------- */ .photography-contact-sheet { counter-reset: contact-frame-no; list-style: none; padding: 1.5rem; margin: 1rem 0 2rem; background: #1c1c1c; border: 1px solid #2a2a2a; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 11rem), 1fr)); gap: 1.25rem; } .contact-frame { counter-increment: contact-frame-no; margin: 0; background: #f5f3ee; padding: 0.5rem 0.5rem 0.4rem; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); transition: transform 120ms ease; } .contact-frame:hover { transform: scale(1.015); } .contact-frame-link { display: block; color: #1c1c1c; text-decoration: none; position: relative; } .contact-frame-img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; display: block; background: #2a2a2a; } .contact-frame-link::before { /* Frame number — analog convention: oldstyle figures, mono, * small, top-left of the print under the image. */ content: counter(contact-frame-no, decimal-leading-zero); position: absolute; top: -1.4em; left: 0; font-family: var(--font-mono, monospace); font-size: 0.75em; color: rgba(245, 243, 238, 0.55); letter-spacing: 0.05em; } .contact-frame-label { display: block; margin-top: 0.35rem; font-size: 0.75em; color: #4a4a4a; line-height: 1.2; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* --------------------------------------------------------------------------- * Series landing — /photography// * --------------------------------------------------------------------------- */ .photo-series-abstract { margin: 1rem 0 0; font-size: 1.05em; line-height: 1.5; color: var(--text); max-width: 36rem; } .photo-series-prose { max-width: 36rem; margin: 1.5rem 0; color: var(--text); }