/*
 * The only stylesheet added on top of the original theme CSS.
 *
 * The five WordPress pages are now sections stacked in the middle column, so
 * all this does is make anchor jumps glide and stop section tops from landing
 * underneath the sticky mobile header.
 */

/*
 * `.page-wrapper` is the scrolling element, not the window — reset.css pins it
 * with `position: fixed; height: 100vh; overflow-y: auto`. Smooth scrolling has
 * to be declared on the element that actually scrolls, so that is where it goes
 * (`html` is kept for good measure).
 */
html,
.page-wrapper {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html,
  .page-wrapper {
    scroll-behavior: auto;
  }
}

/*
 * Each section is wrapped in `.sp-section` to give it an anchor target. That
 * wrapper sits inside `.elementor-widget-wrap`, which Elementor lays out with
 * `display: flex`, and Elementor sizes its own children through
 * `.elementor-element { flex-basis: var(--flex-basis) }`. A plain div gets none
 * of that, so it would size to max-content and burst out of its column — hence
 * the explicit full-width flex basis here.
 */
.sp-section {
  width: 100%;
  flex: 0 0 100%;
  scroll-margin-top: 30px;
}

/* Below 1400px the sticky header overlays the top of the page. */
@media (max-width: 1399px) {
  .sp-section {
    scroll-margin-top: 110px;
  }
}

/*
 * Resume blocks (Experience, Education, Working Skills, Knowledges) are stacked
 * full width instead of paired into columns, so they need their own vertical
 * rhythm — the theme's spacing came from the two-column gutter that no longer
 * exists.
 */
/* `.elementor-container` is a flex row that does not wrap, so two full-width
   columns inside one would shrink to share it. These make them stack. */
.elementor-container.sp-stack {
  flex-wrap: wrap;
}

.sp-stack > .sp-block {
  width: 100%;
  flex: 0 0 100%;
}

.sp-block + .sp-block {
  margin-top: 40px;
}

/* The theme zeroes `.card-text`'s margin, so anything after the place line
   would sit flush against it. */
.bostami-card-item .card-description,
.bostami-card-item .card-keywords {
  margin-top: 10px;
}

.bostami-card-item .card-keywords {
  font-size: 14px;
  opacity: 0.85;
}

/* Role summaries are written to fit, but this keeps a long one from stretching
   the card — four lines and then it truncates. */
.bostami-card-item .card-description {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
}

/* Character counter under the contact form's message box. Turns red while the
   message is still short of the minimum. */
.sp-char-count {
  display: block;
  margin-top: 8px;
  font-size: 13px;
  color: var(--body-text, #6b7280);
  opacity: 0.75;
}

.sp-char-count[data-short="true"] {
  color: #dc3232;
  opacity: 1;
}

/*
 * Client logos. bostami-toolkit.css pins these to `height: 62px !important`
 * with no width limit, which makes the wider marks (ManyPlates is 4.2:1) spill
 * out of their slide. Constrain to the slide and scale to fit instead.
 */
.bostami-client-slider-logo {
  height: auto !important;
  max-height: 60px;
  max-width: 100%;
  width: auto;
  object-fit: contain;
  /* the theme's 15px each side ate into the space the wordmarks need */
  padding: 0 8px !important;
}

/*
 * Page-load curtain: two fabric panels meeting in the middle that part to
 * reveal the page, over roughly five seconds.
 *
 * Driven entirely by CSS so it starts on first paint rather than waiting for
 * hydration — CurtainIntro.tsx only unmounts the node once it has finished.
 * `pointer-events: none` throughout, so even if the teardown never ran the page
 * would still be usable underneath.
 *
 * Timeline: the logo pulses until ~1.5s, fades out, then the panels draw back
 * from 1.8s to 3.8s, gathering their folds tighter as they go.
 */
.sp-curtain {
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  overflow: hidden;
}

.sp-curtain-panel {
  position: absolute;
  top: 0;
  height: 100%;
  /* a hair over half each, so no seam shows between them before they part */
  width: 50.2%;
  background: #ffffff;
  overflow: hidden;
  will-change: transform;
}

/* The folds. A repeating gradient reads as pleated fabric; animating its tile
   width makes the pleats gather as the curtain draws back. */
.sp-curtain-panel::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    90deg,
    rgba(17, 24, 39, 0.075) 0%,
    rgba(17, 24, 39, 0.015) 14%,
    rgba(255, 255, 255, 0) 34%,
    rgba(17, 24, 39, 0.02) 62%,
    rgba(17, 24, 39, 0.085) 86%,
    rgba(17, 24, 39, 0.075) 100%
  );
  background-size: 116px 100%;
  animation:
    sp-curtain-ripple 3.4s ease-in-out infinite alternate,
    sp-curtain-gather 2000ms cubic-bezier(0.65, 0, 0.35, 1) 1800ms forwards;
}

/* Soft depth on the leading edge, so the seam looks like cloth rather than a
   cut. */
.sp-curtain-panel::after {
  content: "";
  position: absolute;
  top: 0;
  height: 100%;
  width: 90px;
}

.sp-curtain-panel-left {
  left: 0;
  animation: sp-curtain-left 2000ms cubic-bezier(0.65, 0, 0.35, 1) 1800ms
    forwards;
}

.sp-curtain-panel-left::after {
  right: 0;
  background: linear-gradient(
    to right,
    rgba(17, 24, 39, 0) 0%,
    rgba(17, 24, 39, 0.11) 100%
  );
}

.sp-curtain-panel-right {
  right: 0;
  animation: sp-curtain-right 2000ms cubic-bezier(0.65, 0, 0.35, 1) 1800ms
    forwards;
}

.sp-curtain-panel-right::after {
  left: 0;
  background: linear-gradient(
    to left,
    rgba(17, 24, 39, 0) 0%,
    rgba(17, 24, 39, 0.11) 100%
  );
}

/* the mark sits on the seam, pulsing, and clears out before the panels move */
.sp-curtain-mark {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: sp-curtain-mark 500ms ease-in 1500ms forwards;
}

.sp-curtain-mark img {
  animation: sp-curtain-pulse 1600ms ease-in-out infinite;
}

@keyframes sp-curtain-left {
  to {
    transform: translateX(-100%);
  }
}

@keyframes sp-curtain-right {
  to {
    transform: translateX(100%);
  }
}

/* pleats sway gently while the curtain is still closed */
@keyframes sp-curtain-ripple {
  from {
    background-position-x: 0;
  }
  to {
    background-position-x: 58px;
  }
}

/* pleats bunch up as the panel draws back */
@keyframes sp-curtain-gather {
  to {
    background-size: 42px 100%;
  }
}

@keyframes sp-curtain-pulse {
  0%,
  100% {
    transform: scale(1);
    opacity: 0.92;
  }
  50% {
    transform: scale(1.09);
    opacity: 1;
  }
}

@keyframes sp-curtain-mark {
  to {
    opacity: 0;
    transform: scale(0.9);
  }
}

/* An intro animation is exactly what reduced-motion is asking us to skip. */
@media (prefers-reduced-motion: reduce) {
  .sp-curtain {
    display: none;
  }
}

/*
 * The signature intro (SignatureIntro.tsx): the name writes itself on a white
 * sheet, then the sheet tears open on a zig-zag and the halves slide apart.
 *
 * Both halves carry the whole animation and are clipped to opposite sides of the
 * same torn seam, so the writing is in step and the tear rips through the
 * signature rather than sliding a finished picture away.
 *
 * Timeline: "Emmanuel" 0.25–1.5s, "Agho" 1.35–2.4s, hold, tear 3.0–4.0s.
 */
.sp-sign {
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  overflow: hidden;
}

.sp-sign-half {
  position: absolute;
  inset: 0;
  background: #ffffff;
  will-change: transform;
  /* Follows the clipped silhouette, so the saw teeth cast a shadow and the two
     pieces read as torn paper rather than as a straight cut through white. */
  filter: drop-shadow(0 0 16px rgba(17, 24, 39, 0.18));
}

/* Both halves pivot on the bottom-left corner, which is what makes the rip
   travel: how far a point separates is proportional to its distance from the
   pivot, so the seam opens at the top right and reaches the bottom left last.
   Only once it has run the length of the page do the pieces fall away. */
.sp-sign-half--tl {
  transform-origin: 0% 100%;
  animation: sp-sign-rip-tl 1400ms cubic-bezier(0.45, 0, 0.2, 1) 2800ms forwards;
}
.sp-sign-half--br {
  transform-origin: 0% 100%;
  animation: sp-sign-rip-br 1400ms cubic-bezier(0.45, 0, 0.2, 1) 2800ms forwards;
}
/* The first two thirds are pure rotation — no translate. That matters: a
   translate separates the whole seam evenly, including at the pivot, and the
   bottom-left corner would part at the same moment as the top-right. Rotating
   alone keeps the near corner shut while the far one opens, which is the tear
   running down the page. Only after it has run do the pieces fall away. */
@keyframes sp-sign-rip-tl {
  0%   { transform: rotate(0deg); }
  8%   { transform: rotate(-0.6deg); }          /* the first catch, at the top */
  30%  { transform: rotate(-2.6deg); }
  62%  { transform: rotate(-7deg); }            /* the tear has run its length */
  100% { transform: translate3d(-70%, -78%, 0) rotate(-13deg); }
}
@keyframes sp-sign-rip-br {
  0%   { transform: rotate(0deg); }
  8%   { transform: rotate(0.6deg); }
  30%  { transform: rotate(2.6deg); }
  62%  { transform: rotate(7deg); }
  100% { transform: translate3d(72%, 80%, 0) rotate(13deg); }
}

/* The signature sits in the middle of the viewport, identically in both halves,
   so the two clipped copies line up into one image. */
.sp-sign-stage {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: min(72vw, 620px);
  /* 900 x 510 — the aspect of agho.png, so the bands below land on the right
     lines whatever the viewport. */
  aspect-ratio: 900 / 510;
}

.sp-sign-line {
  position: absolute;
  inset: 0;
  background: url("../img/agho.png") center / contain no-repeat;
}

/* Each line is a band of the same image, revealed by a wipe in writing order.
   The bands overlap by a couple of per cent: "Agho" reaches up into the first
   line's space and the "l" of Emmanuel comes down into the second's, and a hard
   split would slice through both. */
.sp-sign-line--one {
  animation: sp-sign-write-one 1250ms cubic-bezier(0.35, 0.1, 0.3, 1) 250ms both;
}
.sp-sign-line--two {
  animation: sp-sign-write-two 1050ms cubic-bezier(0.35, 0.1, 0.3, 1) 1350ms both;
}
@keyframes sp-sign-write-one {
  from { clip-path: inset(0 100% 50% 0); }
  to   { clip-path: inset(0 0 50% 0); }
}
@keyframes sp-sign-write-two {
  from { clip-path: inset(46% 100% 0 0); }
  to   { clip-path: inset(46% 0 0 0); }
}

@media (prefers-reduced-motion: reduce) {
  .sp-sign {
    display: none;
  }
}

/*
 * Water bubbles drifting up behind the page. The layer sits between the theme's
 * background images and `.page-wrapper`, so bubbles show in the margins around
 * the content cards but never over them.
 *
 * Each bubble carries its own size, speed, delay and `--sp-drift` inline (see
 * BackgroundBubbles.tsx); the keyframes below are shared.
 */
.sp-bubbles {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.sp-bubble {
  position: absolute;
  /* starts just below the fold and rises out of the top */
  bottom: -160px;
  border-radius: 50%;
  animation-name: sp-bubble-rise;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  /* glass: a bright rim, a highlight off-centre, and a faint cool fill */
  border: 1.5px solid rgba(255, 255, 255, 0.98);
  background:
    radial-gradient(
      circle at 30% 26%,
      rgba(255, 255, 255, 1) 0%,
      rgba(255, 255, 255, 0.52) 20%,
      rgba(255, 255, 255, 0) 50%
    ),
    radial-gradient(
      circle at 68% 76%,
      rgba(158, 206, 255, 0.55) 0%,
      rgba(158, 206, 255, 0.18) 44%,
      rgba(158, 206, 255, 0) 72%
    );
  box-shadow:
    inset -4px -6px 16px rgba(255, 255, 255, 0.75),
    inset 6px 9px 18px rgba(110, 175, 245, 0.3),
    0 4px 18px rgba(17, 24, 39, 0.08);
  will-change: transform;
}

/* Rises the full height while swaying either side of its start column. The
   scale wobble reads as a bubble wobbling in water rather than a rigid disc. */
@keyframes sp-bubble-rise {
  0% {
    transform: translate3d(0, 6vh, 0) scale(0.88);
  }
  25% {
    transform: translate3d(var(--sp-drift, 30px), -20vh, 0) scale(1);
  }
  50% {
    transform: translate3d(calc(var(--sp-drift, 30px) * -0.65), -48vh, 0)
      scale(1.05);
  }
  75% {
    transform: translate3d(var(--sp-drift, 30px), -76vh, 0) scale(1);
  }
  100% {
    transform: translate3d(calc(var(--sp-drift, 30px) * -0.3), -106vh, 0)
      scale(0.9);
  }
}

/* Continuous ambient motion is exactly what reduced-motion asks us to drop. */
@media (prefers-reduced-motion: reduce) {
  .sp-bubbles {
    display: none;
  }
}

/*
 * Hovering a portfolio or blog card spins it a full turn.
 *
 * `perspective()` is inside the transform rather than on the parent, so no
 * wrapper needs a 3D context — which matters because isotope owns the
 * positioning of the portfolio items and slick owns the blog slides.
 *
 * The lift is folded into the keyframes: `.fillter-item:hover` already applies
 * `translateY(-3px)`, and an animation that only rotated would drop the lift for
 * its duration and then snap back to it on the last frame.
 *
 * Backfaces are left visible on purpose — passing through 180° shows the reverse
 * of the card, which is what makes it read as a flip rather than a wobble.
 */
@keyframes sp-card-flip {
  0% {
    transform: perspective(1400px) translateY(0) rotateY(0deg);
  }
  50% {
    transform: perspective(1400px) translateY(-3px) rotateY(180deg);
  }
  100% {
    transform: perspective(1400px) translateY(-3px) rotateY(360deg);
  }
}

.isotop-item:hover .fillter-item,
.single-blog-slider .row > div:hover .blog-slider-single {
  animation: sp-card-flip 900ms ease-in-out;
}

/* keep the blog card's own hover lift consistent with the portfolio one */
.single-blog-slider .row > div .blog-slider-single {
  transition:
    box-shadow 0.25s ease,
    transform 0.25s ease;
}

@media (prefers-reduced-motion: reduce) {
  .isotop-item:hover .fillter-item,
  .single-blog-slider .row > div:hover .blog-slider-single {
    animation: none;
  }
}

/*
 * Cursor sunlight. A faint pool of warm light that follows the pointer, with a
 * soft shadow falling off at its edge.
 *
 * CursorSun.tsx writes the position into `--sp-sun-x/y` and the transform reads
 * it, so the browser only ever recomposites.
 *
 * Deliberately restrained: a small radius, low alphas and a barely-there bloom.
 * It should read as a hint of light on the page, not a spotlight.
 */
.sp-cursor-sun {
  position: fixed;
  top: 0;
  left: 0;
  width: 340px;
  height: 340px;
  margin: -170px 0 0 -170px;
  border-radius: 50%;
  z-index: 9998;
  pointer-events: none;
  opacity: 0;
  transition: opacity 400ms ease;
  transform: translate3d(var(--sp-sun-x, -9999px), var(--sp-sun-y, -9999px), 0);
  background:
    /* warm centre */
    radial-gradient(
      circle closest-side,
      rgba(255, 253, 244, 0.3) 0%,
      rgba(255, 247, 214, 0.17) 16%,
      rgba(255, 240, 190, 0) 34%
    ),
    /* golden body */
    radial-gradient(
        circle closest-side,
        rgba(255, 226, 140, 0.2) 0%,
        rgba(255, 205, 92, 0.11) 32%,
        rgba(255, 186, 66, 0.04) 54%,
        rgba(255, 176, 56, 0) 72%
      ),
    /* the shadow the light falls off into */
    radial-gradient(
        circle closest-side,
        rgba(124, 88, 24, 0) 56%,
        rgba(124, 88, 24, 0.03) 72%,
        rgba(124, 88, 24, 0.012) 86%,
        rgba(124, 88, 24, 0) 100%
      );
  /* just enough spill past the element's bounds to avoid a hard rim */
  box-shadow: 0 0 44px 8px rgba(255, 201, 92, 0.05);
  will-change: transform;
}

/* held under full strength so it stays a hint rather than a highlight */
.sp-cursor-sun[data-visible="true"] {
  opacity: 0.78;
}

/* No pointer to follow on touch, and it is decoration reduced-motion can skip. */
@media (hover: none), (prefers-reduced-motion: reduce) {
  .sp-cursor-sun {
    display: none;
  }
}

/*
 * Widen the content wrapper on desktop. post-149.css caps the stretched
 * section's container at 1450px at top level, and steps it down to 1110px below
 * 1366px — so this override is scoped above that breakpoint to leave the
 * responsive step alone. Four classes deep to outrank
 * `.elementor-149 .elementor-element.elementor-element-7ec232b > .elementor-container`.
 *
 * The three columns are percentage-based (29% / 58.3% / 12%), so they all widen
 * proportionally.
 */
@media (min-width: 1367px) {
  .elementor
    .elementor-section.elementor-section-stretched
    > .elementor-container {
    /*
     * Grow up to 1600px, but never so far that the gutter disappears — the
     * theme kept ~75px each side, and a plain `max-width: 1600px` made the
     * content run edge-to-edge on a 1600px display.
     *
     * `max(1450px, …)` means this only ever widens: below the point where 1600
     * fits with a gutter it falls back to the theme's own 1450px, which the
     * available width then caps exactly as before.
     */
    max-width: max(1450px, min(1600px, 100cqw - 150px));
  }
}

/*
 * Blog hero images. Only two of the theme's images are banner-shaped; the rest
 * are square, and left to themselves they render small and left-aligned while
 * the wide ones fill the column. A fixed height with `cover` gives every post
 * the same banner regardless of the source aspect ratio.
 *
 * `:not(.sp-logo-plate)` keeps this away from the portfolio detail pages, which
 * reuse `.h1-modal-img` for a centred, contained logo.
 */
.h1-modal-img:not(.sp-logo-plate) img {
  width: 100%;
  height: 300px;
  object-fit: cover;
  object-position: center;
}

/*
 * Portfolio cards show brand logos rather than photography. The logos are dark
 * artwork on a transparent background, so they need a white plate behind them —
 * on the card's pastel fill they would be unreadable.
 *
 * The four logos have wildly different aspect ratios (219×52 to 2481×1018), so
 * the plate is a fixed-height box and each logo is contained inside it rather
 * than stretched to fill.
 */
/*
 * Every card is the same size so the grid reads as an even set of tiles rather
 * than a ragged masonry wall. The card is a fixed-height flex column and the
 * logo plate takes whatever is left after the text, so a one-line subtitle and
 * a two-line one still produce identical cards.
 */
.fillter-item {
  display: flex !important;
  flex-direction: column;
  height: 320px;
  background: #ffffff;
  /* The middle column is white too, so the card needs an edge — but only just.
     The shadow does most of the lifting; the border is barely there. */
  border: 1px solid rgba(17, 24, 39, 0.05);
  box-shadow: 0 6px 18px rgba(17, 24, 39, 0.07);
  transition:
    box-shadow 0.25s ease,
    border-color 0.25s ease,
    transform 0.25s ease;
}

.fillter-item:hover {
  border-color: rgba(17, 24, 39, 0.08);
  box-shadow: 0 12px 30px rgba(17, 24, 39, 0.12);
  transform: translateY(-3px);
}

/*
 * The tile at the top of the card. Its tint comes from the project's `logoBg`
 * as an inline style, so each brand keeps its own colour while the card body
 * below stays white.
 */
.sp-logo-plate {
  display: flex !important;
  align-items: center;
  justify-content: center;
  flex: 1 1 auto;
  min-height: 0;
  padding: 24px;
}

.sp-logo-plate img {
  width: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Shown in place of a logo for projects that don't have one yet. */
.sp-logo-fallback {
  font-size: 22px;
  font-weight: 600;
  color: var(--heading-black, #1d1d1d);
  text-align: center;
}

/* The detail page's plate is wider, so it can afford more height. */
.h1-modal-img.sp-logo-plate {
  height: 240px;
}

/*
 * The theme's checkmark list (`.blog-details-list-item-wrap`) sets
 * `line-height: 46px`, which assumes every item fits on one line — a wrapped
 * item ends up with a 46px gap mid-sentence. These make it wrap properly:
 * normal leading, and the checkmark lifted out of the text flow so the second
 * line aligns under the first rather than under the badge.
 */
.h1-modal-paragraph .blog-details-list-item-wrap li {
  line-height: 1.7;
  margin-bottom: 12px;
  padding-left: 28px;
  position: relative;
}

.h1-modal-paragraph .blog-details-list-item-wrap li::before {
  position: absolute;
  left: 0;
  top: 4px;
  margin-right: 0;
}

.h1-modal-paragraph .blog-details-list-item-wrap li:last-child {
  margin-bottom: 0;
}

/* Skill groups sit two to a row; `height: 100%` keeps the pair in each row the
   same height regardless of how long their lists are. */
.skill-group-wrap .col-md-6 {
  margin-bottom: 20px;
}

.skill-group {
  height: 100%;
}

/* reset.css strips `list-style`, `padding` and `margin` off every ul and li on
   the page, so the highlight bullets have to be put back by hand. */
.bostami-card-item .card-highlights {
  margin: 12px 0 0;
  padding-left: 20px;
  list-style: disc;
}

.bostami-card-item .card-highlights li {
  list-style: inherit;
  font-size: 15px;
  line-height: 1.7;
  margin-bottom: 8px;
}

.bostami-card-item .card-highlights li:last-child {
  margin-bottom: 0;
}

/*
 * The /blog/[id] and /portfolio/[id] pages show the same content the modals
 * showed, and every rule for that content is scoped to
 * `.h1-modal-box .modal-dialog .modal-content .modal-body` (and the portfolio
 * equivalent). So the detail pages carry that same class chain to inherit the
 * typography, minus the `.modal` class itself — and these rules undo the parts
 * that made it a floating, scrollable dialog.
 */
.sp-detail,
.sp-detail .modal-dialog,
/* three classes deep so it outranks
   `.h1-modal-box .modal-dialog .modal-content { overflow-y: scroll }` */
.sp-detail .modal-dialog .modal-content {
  /* `.h1-modal-box` / `.portfolio-modal-box` are themselves the fixed, dimmed
     backdrop, so the box needs resetting as much as the dialog inside it. */
  position: static;
  max-width: none;
  width: auto;
  height: auto;
  max-height: none;
  overflow: visible;
  margin: 0;
  padding: 0;
  transform: none;
  pointer-events: auto;
  background: transparent;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/*
 * Elementor's frontend.js used to full-bleed this section by measuring the
 * viewport and writing inline `width`/`left` on it. That whole runtime is not
 * loaded here (see src/app/theme-styles.ts), so the same result is expressed in
 * CSS — which has the bonus of tracking viewport changes without a resize
 * handler.
 *
 * The selector needs both classes to outrank
 * `.elementor-section.elementor-section-stretched { width: 100% }` in
 * custom-frontend.min.css.
 *
 * Container units rather than `vw`: `.page-wrapper` scrolls, so `100vw` counts
 * the scrollbar and would push the section a few pixels off the left edge.
 * `100cqw` measures the full-width wrapper below, which is the width Elementor's
 * JS used to measure.
 */
.bostami-internal-area {
  container-type: inline-size;
}

.elementor-section.elementor-section-stretched {
  width: 100cqw;
  max-width: 100cqw;
  margin-left: calc(50% - 50cqw);
  margin-right: calc(50% - 50cqw);
}
