/*
Theme Name: TWP Theme
Theme URI: https://github.com/airwolff/tiffany-wolff-photography
Author: Andy Wolff
Author URI: https://github.com/airwolff
Description: Block theme for Tiffany Wolff Photography. Ported from the original Astro build; carries Tiffany's four approved design selections (slide-up reveal, zoom + ring hover, centered lightbox with no info panel, split About/Contact).
Tags: block-patterns, block-styles, custom-colors, custom-logo, custom-menu, editor-style, full-site-editing, one-column, photography, portfolio, translation-ready, wide-blocks
Requires at least: 6.7
Tested up to: 6.8
Requires PHP: 7.4
Version: 0.22.0
Text Domain: twp
License: GNU General Public License v3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
*/


/* ============================================================================
   Hand-written CSS layer — twp-theme
   ----------------------------------------------------------------------------
   Tokens (fonts, colors, sizes, spacing, widths) live in theme.json. This file
   is only for what tokens cannot express.

   Galleries are one layout only: the "Masonry" block style on core/gallery.
   Justified rows and the hand-built grid-row patterns were removed in 0.21.0
   — see docs/decisions.md #24. Core's crop rule uses a
   `:not(#individual-image)` specificity hack; the masonry section explains
   how its rules clear it without `!important`.

   BUMP the Version: header on any change here or in patterns/ — WordPress
   caches registered patterns.
   ========================================================================== */


/* -- Site header ----------------------------------------------------------
   Sticky, and shrinks on scroll: the wordmark drops 3.75rem to 1.5rem. The
   `scrolled` class is toggled by assets/js/twp-header.js past 100px.
   ------------------------------------------------------------------------ */

/* The sticky element has to be the template-part wrapper, not .twp-header.
   A sticky box only travels inside its containing block, and .twp-header's
   parent is exactly its own height — so it never moved. */
.wp-site-blocks > header.wp-block-template-part {
	position: sticky;
	top: 0;
	z-index: 50;
	background: var(--wp--preset--color--base);
}

/* Admin bar is fixed at 32px on desktop, 46px on mobile. */
.admin-bar .wp-site-blocks > header.wp-block-template-part { top: 32px; }

@media (max-width: 782px) {
	.admin-bar .wp-site-blocks > header.wp-block-template-part { top: 46px; }
}

.twp-header { background: var(--wp--preset--color--base); }

.twp-header-inner {
	max-width: 1600px;
	margin: 0 auto;
	padding: 2.5rem 32px 1.75rem;
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: 32px;
	will-change: padding;
	transition: padding 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.twp-brand { display: flex; flex-direction: column; line-height: 1; text-decoration: none; }

.twp-brand-name {
	font-family: var(--wp--preset--font-family--display);
	font-size: 2.75rem;
	line-height: 1;
	text-transform: uppercase;
	letter-spacing: 0.02em;
	color: var(--wp--preset--color--contrast);
	will-change: font-size;
	transition: font-size 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.twp-brand-tagline {
	font-family: var(--wp--preset--font-family--body);
	font-weight: 300;
	font-size: 0.6875rem;
	letter-spacing: 0.25em;
	text-transform: uppercase;
	color: #a3a3a3;
	margin-top: 0.5rem;
	transition: font-size 0.45s cubic-bezier(0.4, 0, 0.2, 1),
		margin-top 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Section nav — items come from the saved menu (Appearance → Navigation). */
.twp-header .wp-block-navigation {
	padding-bottom: 0.375rem;
	transition: padding-bottom 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}

.twp-header .wp-block-navigation .wp-block-navigation__container { gap: 2rem; }

.twp-header .wp-block-navigation a {
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.6875rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: #8a8a8a;
	text-decoration: none;
	transition: color 0.2s ease;
}

.twp-header .wp-block-navigation a:hover,
.twp-header .wp-block-navigation .current-menu-item a { color: var(--wp--preset--color--contrast); }

.twp-header.scrolled .twp-header-inner { padding-top: 1rem; padding-bottom: 0.875rem; }
.twp-header.scrolled .twp-brand-name { font-size: 1.375rem; }
.twp-header.scrolled .twp-brand-tagline { font-size: 0.5rem; margin-top: 0.15rem; }
.twp-header.scrolled .wp-block-navigation { padding-bottom: 0; }

@media (max-width: 781px) {
	.twp-header-inner { padding: 1.5rem 20px; }
	.twp-brand-name { font-size: 1.75rem; }
	.twp-header.scrolled .twp-brand-name { font-size: 1.25rem; }
}


/* -- Page content ---------------------------------------------------------
   One wide column. Rows of images stack inside it.
   ------------------------------------------------------------------------ */

.twp-content { padding: 32px; }

@media (max-width: 781px) {
	.twp-content { padding: 20px; }
}


/* -- Scroll reveal: animation C, slide up --------------------------------
   Approved 2026-04-08. Rows rise 30px and fade in as they enter view, the
   three images in a row staggered 50ms apart. `--i` and `is-visible` are set
   by assets/js/twp-reveal.js.

   Everything inside .twp-reveal starts at opacity 0, so if the script fails
   the page is blank rather than merely unanimated. The html.no-js fallback is
   not optional.
   ------------------------------------------------------------------------ */

.twp-reveal {
	opacity: 0;
	transform: translateY(30px);
	transition:
		opacity 0.6s ease calc(var(--i, 0) * 50ms),
		transform 0.6s cubic-bezier(0.4, 0, 0.2, 1) calc(var(--i, 0) * 50ms);
	will-change: transform, opacity;
}

.twp-reveal.is-visible { opacity: 1; transform: none; }

html.no-js .twp-reveal { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
	.twp-reveal { opacity: 1; transform: none; transition: none; }
}


/* -- About: layout B, split, smaller portrait ----------------------------
   Approved 2026-04-08. Portrait fixed left, content scrolling right, portrait
   narrower than the reviewed demo — she asked for it reduced. Demo was 45%,
   this is 34%.
   ------------------------------------------------------------------------ */

.twp-split { display: flex; align-items: flex-start; }

.twp-split-photo {
	width: 34%;
	flex-shrink: 0;
	position: sticky;
	top: 0;
	overflow: hidden;
}

.twp-split-photo figure,
.twp-split-photo .wp-block-image { width: 100%; margin: 0; }
.twp-split-photo img { width: 100%; height: auto; display: block; }

.twp-split-content { flex: 1; min-width: 0; }
.twp-split-content-inner { padding: 0 0 0 56px; max-width: 620px; }

.twp-eyebrow {
	font-size: 0.625rem;
	font-weight: 700;
	letter-spacing: 0.25em;
	text-transform: uppercase;
	color: var(--wp--preset--color--faint);
	margin-bottom: 10px;
}

.twp-divider { height: 1px; background: var(--wp--preset--color--line); margin: 40px 0; border: 0; }

@media (max-width: 781px) {
	.twp-split { flex-direction: column; }
	.twp-split-photo { width: 100%; position: static; }
	.twp-split-content-inner { padding: 32px 0 0; }
}


/* -- Contact form --------------------------------------------------------
   The form is rendered by Fluent Forms — see docs/decisions.md #16. The
   plugin's own stylesheet is deliberately LEFT ENQUEUED: it carries the form's
   structural layout, and dequeuing it collapses the field grid. Everything
   below overrides it on specificity instead. Class names are the plugin's,
   read off rendered output rather than guessed; re-check them after a Fluent
   Forms major update.
   ------------------------------------------------------------------------ */

.twp-content .frm-fluent-form { max-width: 620px; }

.twp-content .frm-fluent-form .ff-el-group { margin: 0 0 18px; }

.twp-content .frm-fluent-form .ff-el-input--label label {
	font-size: 0.625rem;
	font-weight: 700;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
	margin-bottom: 6px;
}

/* The plugin marks required fields with a red asterisk. Muted here — on a
   four-field contact form it reads as an error state, not as guidance. */
.twp-content .frm-fluent-form .ff-el-is-required.asterisk-right label::after {
	color: var(--wp--preset--color--muted);
}

.twp-content .frm-fluent-form .ff-el-form-control {
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.9375rem;
	line-height: 1.6;
	color: var(--wp--preset--color--contrast);
	background: var(--wp--preset--color--base);
	border: 1px solid #ddd;
	border-radius: 0;
	padding: 10px 12px;
	width: 100%;
	box-shadow: none;
	transition: border-color 0.2s;
}

.twp-content .frm-fluent-form .ff-el-form-control:focus {
	outline: none;
	border-color: var(--wp--preset--color--contrast);
	box-shadow: none;
}

.twp-content .frm-fluent-form textarea.ff-el-form-control { resize: vertical; min-height: 120px; }

.twp-content .frm-fluent-form .ff-btn-submit {
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.625rem;
	font-weight: 700;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--wp--preset--color--base);
	background: var(--wp--preset--color--contrast);
	border: 1px solid var(--wp--preset--color--contrast);
	border-radius: 0;
	padding: 14px 32px;
	cursor: pointer;
	transition: background-color 0.2s, color 0.2s;
}

.twp-content .frm-fluent-form .ff-btn-submit:hover {
	background: transparent;
	color: var(--wp--preset--color--contrast);
}

/* Validation and the post-submit confirmation. Red is the one place this
   theme uses a colour outside the palette; it has to read as an error. */
.twp-content .frm-fluent-form .error-text,
.twp-content .frm-fluent-form .text-danger {
	font-size: 0.75rem;
	letter-spacing: 0.02em;
	color: #b3261e;
}

.twp-content .ff-message-success {
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.9375rem;
	line-height: 1.7;
	color: var(--wp--preset--color--contrast);
	background: transparent;
	border: 1px solid #ddd;
	border-radius: 0;
	padding: 20px 24px;
}

/* -- Footer --------------------------------------------------------------
   White, and one inline row. Was a dark centred stack; Andy asked for both to
   change 2026-08-23.

   Contrast on #ffffff: #111111 is 18.9:1 and #8a8a8a is 3.5:1. The grey is
   below the 4.5:1 AA threshold for body text, so it is only used at 0.6875rem
   with wide tracking for the two secondary lines, and the email — the one
   thing anybody clicks — is full contrast. Re-check if these colors change.
   ------------------------------------------------------------------------ */

.twp-footer {
	background: var(--wp--preset--color--base);
	border-top: 1px solid var(--wp--preset--color--line);
	margin-top: 80px;
}

.twp-footer-inner {
	max-width: 1600px;
	margin: 0 auto;
	padding: 28px 32px;
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: 24px;
}

.twp-footer p { margin: 0; }

.twp-footer .twp-footer-contact a {
	font-size: 0.6875rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--wp--preset--color--contrast);
}

.twp-footer .twp-footer-copy {
	font-size: 0.6875rem;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: #8a8a8a;
}

.twp-footer a { text-decoration: none; transition: color 0.2s; }
.twp-footer a:hover { color: #8a8a8a; }

@media (max-width: 781px) {
	.twp-footer-inner {
		flex-direction: column;
		align-items: center;
		text-align: center;
		gap: 10px;
		padding: 28px 20px;
	}
}


/* -- Links inside styled blocks -----------------------------------------
   Core renders site-title as a wrapper with an <a> inside; the class lands on
   the wrapper, so the link has to inherit.
   ------------------------------------------------------------------------ */

.twp-brand-name a { color: inherit; font: inherit; letter-spacing: inherit; text-decoration: none; }


/* -- Sticky footer -------------------------------------------------------
   Short pages otherwise leave the footer floating with white below it.
   ------------------------------------------------------------------------ */

.wp-site-blocks { display: flex; flex-direction: column; min-height: 100vh; }
.wp-site-blocks > main, .wp-site-blocks > .twp-main { flex: 1 0 auto; }


/* -- Focus visibility (WCAG 2.4.7) --------------------------------------
   The design leans on low-contrast greys; without this, keyboard focus is
   effectively invisible.
   ------------------------------------------------------------------------ */

a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible {
	outline: 2px solid var(--wp--preset--color--contrast);
	outline-offset: 2px;
}

.twp-lb button:focus-visible { outline-color: #fff; }


/* -- Masonry gallery -----------------------------------------------------
   Added 0.9.0. A core Gallery block with the theme's "Masonry" block style
   applied, laid out with CSS multi-columns. No script, no extra markup, no
   competing layout layer — the whole thing is the rules below.

   WHY THIS WORKS WHERE EIGHT EARLIER TRIES DID NOT
   ------------------------------------------------
   Core's crop rule is scoped to `.is-cropped`:

     .wp-block-gallery.has-nested-images.is-cropped
       figure.wp-block-image:not(#individual-image) img
       { flex: 1 0 0%; height: 100%; object-fit: cover; width: 100% }

   That `object-fit: cover` is what stretched every photo down its column, and
   it is only ever switched on by the block's own "Crop images" toggle. The
   pattern ships with it off, so in normal use the rule never applies at all.
   The `.is-cropped` selectors below are the safety net for a gallery someone
   turns cropping back on for, or one pasted in from elsewhere.

   Specificity, counted rather than guessed. Core's rule is one ID (the
   `:not(#individual-image)` hack) + four classes + two types. Repeating that
   shape with `is-style-twp-masonry` added makes five classes, which outranks
   it. Nothing here needs `!important` — deliberately, because `!important`
   would also outrank the editor's Dimensions panel and lock the page author
   out of her own gutters.

   Gutters follow core's own gallery gap variable, which the block sets inline
   from the Dimensions panel. The fallback is spacing size 4 (40px) rather than
   core's 16px, which is too tight for photographs at this scale.
   ------------------------------------------------------------------------ */

figure.wp-block-gallery.is-style-twp-masonry {
	--twp-masonry-gap: var(--wp--style--unstable-gallery-gap, var(--wp--preset--spacing--40, 40px));

	/* Cancels the flex layout core gives every gallery. Multi-columns need a
	   block container; `columns` on a flex container does nothing. */
	display: block;
	columns: 1;
	column-gap: var(--twp-masonry-gap);
}

/* One ID + four classes + one type — outranks core's width rule, which is one
   ID + three classes + one type. */
.wp-block-gallery.is-style-twp-masonry.has-nested-images figure.wp-block-image:not(#individual-image) {
	display: block;
	width: 100%;
	margin: 0 0 var(--twp-masonry-gap);

	/* The one rule that actually makes it masonry rather than a sliced-up
	   mess: without it a tall photo is split across a column boundary. */
	break-inside: avoid;
	-webkit-column-break-inside: avoid;

	/* Both also required by the hover treatment below. */
	position: relative;
	overflow: hidden;
	background: #ededed;
}

/* Trailing gutter under the last item in a column reads as a ragged bottom
   edge; multi-columns balance better without it. */
.wp-block-gallery.is-style-twp-masonry.has-nested-images figure.wp-block-image:not(#individual-image):last-child {
	margin-bottom: 0;
}

/* Let every photo keep its own aspect ratio. The second selector is the one
   that beats core's crop rule — five classes to its four. */
.wp-block-gallery.is-style-twp-masonry.has-nested-images figure.wp-block-image:not(#individual-image) img,
.wp-block-gallery.is-style-twp-masonry.has-nested-images.is-cropped figure.wp-block-image:not(#individual-image) img {
	display: block;
	width: 100%;
	height: auto;
	object-fit: fill;
	flex: none;
}

/* Core turns the figure's link/wrapper into a flex row under .is-cropped so
   the image can stretch. Undo it, or the link collapses to zero height. */
.wp-block-gallery.is-style-twp-masonry.has-nested-images.is-cropped figure.wp-block-image:not(#individual-image) > a,
.wp-block-gallery.is-style-twp-masonry.has-nested-images.is-cropped figure.wp-block-image:not(#individual-image) > div {
	display: block;
}

/* Hover — Tiffany's option K (zoom + inset ring, both 400ms), approved
   2026-04-08. Not a new design choice; the approved one, applied here. */
.wp-block-gallery.is-style-twp-masonry figure.wp-block-image img {
	transform: scale(1);
	transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
	will-change: transform;
}

.wp-block-gallery.is-style-twp-masonry figure.wp-block-image:hover img { transform: scale(1.05); }

.wp-block-gallery.is-style-twp-masonry figure.wp-block-image::after {
	content: "";
	position: absolute;
	inset: 0;
	box-shadow: inset 0 0 0 0 #fff;
	transition: box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1);
	pointer-events: none;
	z-index: 1;
}

.wp-block-gallery.is-style-twp-masonry figure.wp-block-image:hover::after { box-shadow: inset 0 0 0 6px #fff; }

/* Column count. One on phones, two on tablets, and on desktop whatever the
   block's own Columns slider says — core writes that out as `columns-N`, so
   honouring the class keeps the editor control working instead of hardcoding
   a number the page author cannot change. 600px and 782px are core's own
   gallery and column breakpoints; reusing them keeps the reflow consistent
   with the rest of the site. */
@media (min-width: 600px) {
	figure.wp-block-gallery.is-style-twp-masonry { columns: 2; }
}

@media (min-width: 782px) {
	figure.wp-block-gallery.is-style-twp-masonry,
	figure.wp-block-gallery.is-style-twp-masonry.columns-default { columns: 3; }

	figure.wp-block-gallery.is-style-twp-masonry.columns-1 { columns: 1; }
	figure.wp-block-gallery.is-style-twp-masonry.columns-2 { columns: 2; }
	figure.wp-block-gallery.is-style-twp-masonry.columns-3 { columns: 3; }
	figure.wp-block-gallery.is-style-twp-masonry.columns-4 { columns: 4; }
	figure.wp-block-gallery.is-style-twp-masonry.columns-5 { columns: 5; }
	figure.wp-block-gallery.is-style-twp-masonry.columns-6 { columns: 6; }
	figure.wp-block-gallery.is-style-twp-masonry.columns-7 { columns: 7; }
	figure.wp-block-gallery.is-style-twp-masonry.columns-8 { columns: 8; }
}

@media (prefers-reduced-motion: reduce) {
	.wp-block-gallery.is-style-twp-masonry figure.wp-block-image img,
	.wp-block-gallery.is-style-twp-masonry figure.wp-block-image::after { transition: none; }
}


/* -- Testimonials --------------------------------------------------------
   Three layouts, one type system: the quote in the display serif, the
   attribution small, uppercase and wide-tracked so it reads as a label rather
   than as more sentence. Patterns are twp/testimonials-quote,
   twp/testimonials-columns and twp/testimonials-stacked.

   Spacing lives here and not in the patterns, per patterns/_readme.md — baked
   spacing would override the editor's Dimensions panel.
   ------------------------------------------------------------------------ */

.twp-testimonial-body {
	font-family: var(--wp--preset--font-family--display);
	color: var(--wp--preset--color--contrast);
	text-wrap: pretty;
}

.twp-testimonial-cite {
	font-family: var(--wp--preset--font-family--body);
	font-size: 0.625rem;
	font-weight: 700;
	letter-spacing: 0.18em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
}

/* -- A. single pull quote: the loudest of the three, so it is also the
      quietest typographically — no quote marks, no rules, just size. */
.twp-testimonial-quote {
	max-width: 900px;
	margin-inline: auto;
	padding-block: 96px;
	text-align: center;
}

.twp-testimonial-quote .twp-testimonial-body {
	font-size: clamp(1.375rem, 2.4vw, 2rem);
	line-height: 1.45;
	margin: 0 0 32px;
}

.twp-testimonial-quote .twp-testimonial-cite { margin: 0; }

/* -- B. three across. */
.twp-testimonial-row { padding-block: 80px; gap: 64px; }

.twp-testimonial-row .twp-testimonial-body {
	font-size: 1.0625rem;
	line-height: 1.6;
	margin: 0 0 20px;
}

.twp-testimonial-row .twp-testimonial-cite { margin: 0; }

/* Core stacks columns below 782px. Restore vertical rhythm when it does —
   without this the three quotes run together as one block of text. */
@media (max-width: 781px) {
	.twp-testimonial-row { gap: 48px; }
}

/* -- C. stacked narrative. */
.twp-testimonial-stack {
	max-width: 780px;
	margin-inline: auto;
	padding-block: 80px;
}

.twp-testimonial-stack .twp-testimonial-body {
	font-size: 1.1875rem;
	line-height: 1.65;
	margin: 0 0 20px;
}

.twp-testimonial-stack .twp-testimonial-cite { margin: 0; }

.twp-testimonial-stack .twp-divider {
	margin-block: 56px;
	border: 0;
	border-top: 1px solid var(--wp--preset--color--line);
}


/* -- Testimonial slider --------------------------------------------------
   Progressive enhancement. Everything here hangs off .is-slider, which
   assets/js/twp-slider.js adds only once it has successfully wired a slider
   up. Without the script none of this applies and the testimonials render as
   the plain stacked list above — deliberately, so a script failure never
   hides the words behind controls that are not working.
   ------------------------------------------------------------------------ */

.twp-slider.is-slider { overflow: hidden; position: relative; }

.twp-slider.is-slider .twp-slider-track {
	display: flex;
	flex-wrap: nowrap;
	column-gap: 64px;
	will-change: transform;
}

/* Widths are set inline by the script, which reads this gap back to do the
   arithmetic. Changing the gap here is enough; do not also change the JS. */
.twp-slider.is-slider .twp-testimonial-item { min-width: 0; }

/* Off-screen slides stay in the flow (the track is translated, not
   re-rendered) but must not be readable by assistive tech. */
.twp-slider.is-slider .twp-testimonial-item[aria-hidden="true"] {
	visibility: hidden;
	transition: visibility 0s linear 0.6s;
}

.twp-slider.is-slider .twp-testimonial-item[aria-hidden="false"] {
	visibility: visible;
	transition: visibility 0s;
}

.twp-slider-nav {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 20px;
	margin-top: 48px;
}

.twp-slider-arrow {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 36px;
	height: 36px;
	padding: 0;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: 50%;
	background: transparent;
	color: var(--wp--preset--color--muted);
	cursor: pointer;
	transition: color 0.2s, border-color 0.2s;
}

.twp-slider-arrow:hover {
	color: var(--wp--preset--color--contrast);
	border-color: var(--wp--preset--color--contrast);
}

.twp-slider-dots { display: flex; align-items: center; gap: 10px; }

.twp-slider-dot {
	width: 6px;
	height: 6px;
	padding: 0;
	border: 0;
	border-radius: 50%;
	background: var(--wp--preset--color--line);
	cursor: pointer;
	transition: background-color 0.2s, transform 0.2s;
}

.twp-slider-dot[aria-current="true"] {
	background: var(--wp--preset--color--contrast);
	transform: scale(1.4);
}

/* The arrows are the only controls a keyboard user has here, so their focus
   ring is not optional and must not be removed with the rest of the chrome. */
.twp-slider-arrow:focus-visible,
.twp-slider-dot:focus-visible {
	outline: 2px solid var(--wp--preset--color--contrast);
	outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
	.twp-slider.is-slider .twp-slider-track { transition: none !important; }
	.twp-slider-dot { transition: none; }
}

@media (max-width: 781px) {
	.twp-slider.is-slider .twp-slider-track { column-gap: 32px; }
	.twp-slider-nav { margin-top: 32px; }
}
