📸 Photo Studio

A self-hosted photo & motion API: AI background removal, face-aware compositing, a full raster-edit pipeline, and reel-native video effects — one Docker container.

FastAPI + Docker rembg · 5 cutout models smart compositing 26 edit ops · news-illustration v2 diptych · inset · montage 7 motion effects → MP4 / animated WebP CPU-only, runs on the scraper ● live · foto-api.autonomousmedia.io

01What it does

Five capability groups behind a small HTTP API. Everything runs on CPU and is built for batch jobs on the scraper server. iPhone HEIC inputs are accepted everywhere.

✂️ Cutout

AI background removal via rembg. Five models from fast people-segmentation to BiRefNet portrait matting with clean hair edges. Returns a transparent PNG.

🪄 Compose

Drop one or more subjects onto a new background. Face/profile detection auto-decides side, scale and gaze — or place manually, or let a vision model decide.

🎛️ Edit

An ordered pipeline of 26 raster ops: resize, crop, colour, filters — plus the news-illustration v2 set: duotone, sticker outline, flatten-bg colour block, silhouette, scanlines, glitch, grain, paper texture.

🧩 News layouts

Three layout endpoints for editorial graphics: side-by-side diptych ("orsak | verkan"), circular detail inset with rim, and N-layer montage (the expert-collage pattern).

🎬 Motion

Seven reel-native effects: Ken Burns, parallax, kinetic captions, karaoke subtitles, count-up data, whip/glitch transitions, spin cinemagraph — rendered to MP4 or seamless animated WebP.

🗞️ Styles

Three editorial "news illustration" looks rendered from a photo + its cutout mask: torn-paper split-face, red sticker cutout, and neon money glow — ready for social posts.

02Background removal

One call strips the background and returns a transparent PNG. BiRefNet keeps fine hair and edge detail clean.

original
Original photo
cutout
Cutout (BiRefNet portrait) — transparent background
# model: u2net_human_seg (default, fast) · birefnet-portrait (best edges)
curl -F file=@photo.jpg "https://foto-api.autonomousmedia.io/v1/cutout?model=birefnet-portrait" -o cutout.png

03Smart compositing

Place a cut-out subject onto a new scene. In auto mode, face & profile detection picks the side, scale, and which way the subject should face — so the gaze leads into the frame and the scene's focal point stays visible.

Auto mode — zero config

subject
Subject
background
Background
composited
Auto result — placed front-left, facing the path & water
curl -F background=@path.jpg -F subjects=@man.jpg \
     -F 'params={"mode":"auto","cutout_model":"birefnet-portrait"}' \
     https://foto-api.autonomousmedia.io/v1/compose -o out.jpg

Multiple subjects & manual control

Pass several subjects in one call. Manual mode gives per-subject control of scale, position, facing and draw order (depth).

two-person composite
Two subjects composited onto one scene (manual placement, draw order = depth)
curl -F background=@path.jpg -F subjects=@man.jpg -F subjects=@woman.jpg \
     -F 'params={"mode":"manual","cutout_model":"birefnet-portrait","items":[
        {"scale":0.86,"target_frac":0.26,"draw_order":1,"warm":1.01},
        {"scale":0.60,"target_frac":0.60,"base_frac":0.99,"draw_order":0}
     ]}' \
     https://foto-api.autonomousmedia.io/v1/compose -o combo.jpg

Three placement modes

ModeHow placement is decidedCost
autoFace + profile detection (OpenCV). Picks side, scale, gaze. Default.Free, fast, scales infinitely
manualYou supply explicit per-subject params (items).Free
llmA vision model looks at the scene and decides placement. Opt-in.Tokens per image — needs ANTHROPIC_API_KEY
Manual items fields: scale (0–1 of frame height) · target_frac (0–1, where the face sits horizontally) · base_frac (0–1, vertical position of the subject's feet/bottom — lower value = further back) · facing · flip · draw_order (lower = behind) · feather · warm (<1 cooler, >1 warmer) · color_match (0–0.4, pull toward scene colour).

04General edits

Send an ordered list of operations; they apply in sequence. Good for thumbnails, normalisation, watermarks-free filters, format conversion.

original
grayscale
sepia
autocontrast + saturation + vignette
curl -F file=@photo.jpg \
     -F 'ops=[{"op":"resize","max":1600},{"op":"autocontrast"},{"op":"saturation","factor":1.3},{"op":"vignette","strength":0.45}]' \
     -F output_format=jpeg -F quality=90 \
     https://foto-api.autonomousmedia.io/v1/edit -o edited.jpg

Available operations

opparams
resizew / h / max (bounds long edge, keeps aspect)
cropbox=[l,t,r,b] or aspect="16:9" (centre crop)
rotate / flipdeg · axis="h"|"v"
brightness · contrast · saturation · sharpnessfactor (1.0 = unchanged)
blurradius
grayscale · sepia · invert · equalize
autocontrastcutoff (percent clipped, default 1)
posterizebits (1–8)
vignettestrength (0–1)
bordersize, color="#rrggbb"
convertformat="jpeg"|"png"|"webp"

News-illustration ops (v2)

Built for the story→illustration pipeline: duotone category colours, Axios-style colour blocks, sticker cutouts, texture passes. RGBA cutout alpha survives the chain — cutout → grayscale → flatten_bg(color) renders the colour-block portrait in one call.

opparams
duotoneshadow="#rrggbb", highlight="#rrggbb" — luminance mapped onto a 2-color ramp
sticker_outlinewidth (px, default 12), color="#ffffff" — RGBA cutouts only; dilated alpha edge
flatten_bgcolor="#rrggbb" — composite an RGBA cutout onto a solid color (Axios-style block)
silhouettefill="#1a1a1a", threshold (alpha 0–255, default 32) — RGBA cutout to flat shape
scanlinesspacing (px, default 4), opacity (0–1, default 0.25) — CRT/VHS rows
glitch_stillstrength (0–1, default 0.5), seed — RGB channel shift + slice offsets
grainstrength (0–1, default 0.25), seed — film grain noise
paper_texturestrength (0–1, default 0.3), seed — fibrous paper light/dark mottle
# Axios colour-block portrait in one call (RGBA cutout in, JPEG out)
curl -F file=@cutout.png \
     -F 'ops=[{"op":"grayscale"},{"op":"contrast","factor":1.15},{"op":"flatten_bg","color":"#1f3a6b"}]' \
     https://foto-api.autonomousmedia.io/v1/edit -o block.jpg

Smart crop: face-aware, never beheads

Aspect crop anchored on the face/subject centre with the R6 crop-safety bar (≥8% of crop height above the topmost crown, ≥5% on every other face edge). When no window at the target aspect can hold every face, it answers 422 with {"crop_safe": false} instead of cutting — the caller keeps its text-card fallback. Faceless images get a plain centre-crop, so it is a drop-in for geometric centercrop.

POST/v1/smart-crop
fieldtypenotes
imagefilethe source photo
paramsform (JSON)width, height (target px box), headroom (default 0.08), edge (default 0.05), output_format, quality

Success responses carry X-Crop-Safe, X-Faces, X-Focal-Point (percent) and X-Crop-Window (source px) headers.

# TT photo → 620×196 read-strip crop that keeps every face whole
curl -F image=@tt_photo.jpg -F 'params={"width":620,"height":196}' \
     https://foto-api.autonomousmedia.io/v1/smart-crop -o strip.jpg

News layouts: diptych · inset · montage

Three layout endpoints that compose finished editorial graphics from images you already have (raw photos or RGBA cutouts).

POST/v1/diptych
fieldtypenotes
image_a · image_bfilethe two panels ("orsak | verkan", lag A | lag B)
paramsform (JSON)width, height, split (0–1), gap, gap_color, output_format, quality
POST/v1/inset
fieldtypenotes
background · insetfilebackground = the scene, inset = the circular detail
paramsform (JSON)size (fraction of height), pos ("left"|"right"|"center" or [x,y] 0–1), rim, rim_color, width, height
POST/v1/montage
fieldtypenotes
imagesfile[]all layers, in order (repeat the field); send cutout layers as RGBA PNG
paramsform (JSON)width, height, layers:[{image, role, pos, scale, outline, outline_color, circle, rotate}] — bg + N positioned layers (the expert-collage pattern)

05Motion / reels

Seven reel-native effects rendered server-side to 9:16 MP4 — or, with params.output="webp", to a seamless animated WebP loop (alpha-capable, autoplay-safe; the cinemagraph format). Built for a stills + TTS news pipeline (no After Effects, no manual editing). Each is a pure function of time → frame; the same model maps 1:1 to a Remotion useCurrentFrame() setup if you move rendering to Node later. The clips below are produced by the service.

Ken Burnsphoto → motion · slow zoom + drift on a still
Parallaxreuses the cutout · subject moves faster than the background
Kinetic captionshighest ROI · word-pop with spring overshoot
Karaoke subtitlesTTS word-sync highlight
Count-up datanumber ticks up + bars grow in
Whip / glitchfast slide + blur spike + RGB split
Two effects depend on word-level timing. Kinetic captions and karaoke land best when driven off per-word timestamps — pass words:[{"w":"Räntan","t":0.0},…] from your ElevenLabs with_timestamps response (or WhisperX). Without timings they fall back to an even beat.
POST/v1/motion/{effect}
fieldtypenotes
effectpathken_burns · parallax · captions · karaoke · countup · transition · spin
imagefilerequired for ken_burns/parallax/spin; optional background for the others
image2filesecond scene for transition · the rotating RGBA element for spin
paramsform (JSON)common: w, h, fps, duration, output ("mp4" default · "webp" animated loop) · plus per-effect (below)

video/mp4 (H.264, 9:16 by default) or image/webp (animated) when output="webp".

Per-effect params

effectparams
ken_burnszoom (1.16) · pan ("up-left"…) · kicker · headline
parallaxcutout_model · kicker · headline
captionswords (list) · highlight (index) · eyebrow · beat (s)
karaokewords (list or [{w,t}]) · gap (s) · lead (s)
countuptitle · corner · value · delta · bars (list) · labels (list)
transitionscene_a/scene_b {kicker,headline,tint} — or pass image+image2
spincinemagraph: static cover-cropped base (image) + one RGBA element (image2) rotating in place · rpm (20) — loop-seamless defaults: 12 fps, 3 s, 20 rpm = exactly one turn
# Ken Burns on a still, with a lower-third headline
curl -F image=@photo.jpg \
     -F 'params={"duration":4,"zoom":1.18,"kicker":"Stockholm · 06 juni","headline":"Stadshuset i kvällsljus"}' \
     https://foto-api.autonomousmedia.io/v1/motion/ken_burns -o kenburns.mp4

# Kinetic captions driven by word timings from TTS
curl -F 'params={"words":["Räntan","sänks","med 0,25","i juni"],"highlight":2}' \
     https://foto-api.autonomousmedia.io/v1/motion/captions -o captions.mp4

# Count-up data card from your pipeline JSON
curl -F 'params={"title":"OMXS30 · stängning","value":2487.6,"delta":1.4,
        "bars":[-0.8,1.2,0.9,-0.4,1.4],"labels":["VOLVO","EVO","SBB","SINCH","ATCO"]}' \
     https://foto-api.autonomousmedia.io/v1/motion/countup -o data.mp4

06Editorial styles

Three "news illustration" looks applied automatically to an ordinary photo. The subject is cut out (rembg) and the art — duotone colour ramps, torn-paper seams, sticker strokes, neon edge glow, halftone, scanlines — is composited around the mask. One photo, three social-ready looks:

source photo
Source photo (input)
split style
split — torn-paper warm/cool split-face on rust · 1080×1080
red style
red — white-outline sticker, red duotone, splatter bg · 1080×1350
money style
money — teal duotone, neon-green glow, $-tile, scanlines · 1600×900
Mask source. The original experiments used an Apple Vision Swift tool for the cutout; here the mask is the alpha from the built-in rembg cutout, so it runs anywhere (and keeps every person in multi-subject shots). Tune via cutout_modelu2net_human_seg (default, fast) or birefnet-portrait (cleaner edges).
POST/v1/style/{style}
fieldtypenotes
stylepathsplit · red · money
filefilethe photo (JPG/PNG/HEIC)
cutout_modelquerydefault u2net_human_seg
qualityqueryJPEG quality, default 92

image/jpeg at the style's native social size.

curl -F file=@photo.jpg "https://foto-api.autonomousmedia.io/v1/style/red" -o red.jpg
curl -F file=@photo.heic "https://foto-api.autonomousmedia.io/v1/style/money?cutout_model=birefnet-portrait" -o money.jpg

08API reference

All endpoints return the resulting media bytes (PNG/JPEG/WebP/MP4) except the JSON discovery routes. Interactive Swagger UI is at /swagger.

Base URL: the examples use the live endpoint https://foto-api.autonomousmedia.io (public, TLS via Let's Encrypt). For local development, swap it for http://localhost:8000.
methodroutereturns
GET/healthJSON — liveness + capabilities
GET/v1/modelsJSON — models, ops, effects, modes
POST/v1/cutoutimage/png (transparent)
POST/v1/composeimage/jpeg (or png/webp)
POST/v1/editimage/jpeg (or png/webp) — 26 ops incl. news-illustration v2
POST/v1/smart-cropimage/jpeg — face-aware aspect crop; 422 JSON when no safe window
POST/v1/diptychimage/jpeg — side-by-side two-panel layout
POST/v1/insetimage/jpeg — scene + circular detail inset
POST/v1/montageimage/jpeg — N-layer expert-collage composite
POST/v1/motion/{effect}video/mp4 (9:16) · image/webp animated when output="webp"
POST/v1/style/{style}image/jpeg (editorial look)
GET/health

Liveness + capability snapshot (models, ops, whether LLM mode is enabled).

GET/v1/models

Lists cutout models, edit ops (with param hints), and available compose modes.

POST/v1/cutout
fieldtypenotes
filefilethe image (multipart)
modelqueryu2net_human_seg (default) · u2net · isnet-general-use · birefnet-portrait · birefnet-general
alpha_mattingquerybool, default true — softer, cleaner edges

image/png with alpha.

POST/v1/compose
fieldtypenotes
backgroundfilethe scene
subjectsfile[]one or more (repeat the field). Raw photos are auto-cut; pre-cut RGBA PNGs are used as-is.
paramsform (JSON)mode, cutout_model, auto_cutout, output_format, quality, items[]

image/jpeg (or PNG/WebP via output_format).

POST/v1/edit
fieldtypenotes
filefilethe image
opsform (JSON)array of {"op":...} objects, applied in order
output_format · qualityformdefault jpeg · 94
POST/v1/diptych · /v1/inset · /v1/montage

News-layout composites. Full fields & params in §04 General edits → News layouts. → image/jpeg (or png/webp).

POST/v1/motion/{effect}

Renders a 9:16 MP4 — or an animated-WebP loop with params.output="webp". Seven effects incl. the spin cinemagraph. Full fields & per-effect params in §05 Motion.

POST/v1/style/{style}

Applies an editorial look (split/red/money). Details in §06 Editorial styles. → image/jpeg.

Python client

import requests, json
B = "https://foto-api.autonomousmedia.io"

# 1) cut a subject out
png = requests.post(f"{B}/v1/cutout", params={"model":"birefnet-portrait"},
                    files={"file": open("me.jpg","rb")}).content
open("me.png","wb").write(png)

# 2) composite onto a new scene (auto placement)
r = requests.post(f"{B}/v1/compose",
      files=[("background",open("scene.jpg","rb")),
             ("subjects",open("me.jpg","rb"))],
      data={"params": json.dumps({"mode":"auto"})})
open("out.jpg","wb").write(r.content)

09Deploy

Local

docker compose up -d --build
# → http://localhost:8000  (docs at /, Swagger at /swagger)
curl -s localhost:8000/health | jq

Scraper server

Heavy/batch image work belongs on the scraper. Ship the repo to /opt/scrapers/photo-studio/ and bring it up with Compose. (The docs/ page is bind-mounted — HTML edits go live with just the rsync, no rebuild.)

rsync -az --exclude .git ./ sandenskog@SCRAPER_IP:/opt/scrapers/photo-studio/
ssh sandenskog@SCRAPER_IP "cd /opt/scrapers/photo-studio && docker compose up -d --build"
Model storage. The default model is baked into the image; BiRefNet (~1 GB) downloads on first use into the models Docker volume so it persists across restarts. To bake it in too, build with --build-arg PREFETCH_BIREFNET=1.

Enable the vision-LLM placement mode (optional)

# in docker-compose.yml → environment:
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
PLACEMENT_MODEL: claude-haiku-4-5   # cheap default; bump to claude-opus-4-8 for max quality

When set, {"mode":"llm"} becomes available on /v1/compose and /health reports llm_placement:true.

10Notes & limits

11CSS over photos — feed recipes

Not API calls — these are front-end CSS the feed renders live over a photo. The API serves the image; the browser does the rest. Same family of tomfoolery, kept on one page so the whole visual toolbox lives in one place. Every demo below runs right here on real photos; the code under each one is the entire recipe.

Two editorial rules baked into all of them. Keep a readability floor (a scrim or a solid-colour fallback) and respect prefers-reduced-motion — the scroll/slider effects must degrade to a still. Knockout and depth headlines should stay real <h1> text (not baked pixels) so screen readers and search still read the headline.

Standard headline treatments — the default tier (~60% of cards)

1 · Headline on photo (gradient scrim)

hero
Nöje

Festivalsommaren är äntligen här

/* the everyday hero: text on the photo, made readable by a scrim */
.hero{position:relative;color:#fff}
.hero .cap{position:absolute;left:0;right:0;bottom:0;padding:20px 16px;
  background:linear-gradient(to top,rgba(0,0,0,.85),transparent 60%);}  /* readability floor */
.hero h1{margin:0;font:800 28px system-ui;text-shadow:0 1px 12px rgba(0,0,0,.5)}

The bread-and-butter card: headline sits directly on the image. How: a bottom-up linear-gradient scrim (or a text-shadow) guarantees contrast no matter what the photo does underneath. Add a small uppercase kicker in the section colour above the headline. Watch: never skip the scrim — white text on a bright photo is the classic readability fail.

2 · Headline on a colour plate

plate
Musik

Rekordpublik på årets festival

/* solid section-colour box sitting on the photo — max readability */
.plate{position:relative}
.plate .box{position:absolute;left:16px;right:16px;bottom:16px;
  background:#1f3a6b;color:#fff;padding:12px 15px;border-radius:8px;}  /* swap colour per section */
.plate h1{margin:0;font:800 26px system-ui}

Headline drops onto a solid colour plate over the photo — the safest, highest-contrast default, and the plate colour doubles as the section signal. How: an absolutely-positioned block with the section background; the photo stays clean above it. Watch: works on any photo (the plate carries the text), so it's the go-to when the image is busy or low-contrast.

3 · Lower-third bar

lower third

Hien om landslagsdrömmen

/* TV-style strap across the lower third */
.lower .bar{position:absolute;left:0;right:0;bottom:16px;
  background:rgba(15,17,21,.76);backdrop-filter:blur(3px);
  border-left:4px solid var(--accent);padding:9px 15px;}

A translucent strap across the lower third with an accent edge — reads as "broadcast/now". How: a full-width semi-transparent bar with backdrop-filter:blur so the photo stays visible through it, plus a coloured border-left as the section tick. Watch: keep it to the lower third so it never covers the subject's face.

Signature moves — the feature & showstopper tiers

4 · Knockout headline — photo through the type

FESTEN

/* headline becomes a window onto the photo */
.knockout{
  background: url(/photo.jpg) center/cover;
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  font: 800 12vw/.85 system-ui, sans-serif;
}

The text is transparent and the photo shows through the letters — type and image become one object instead of two stacked rectangles. How: paint the photo as the element's background, then background-clip:text + color:transparent clips that paint to the glyph shapes. Watch: needs a busy, tonal photo behind it; on a flat sky it reads as nothing — always keep a solid-colour fallback for contrast.

5 · Duotone recolour (section colour system)

concert
/* grayscale base + two blend layers = live duotone, no re-render */
.duo{position:relative}
.duo img{filter:grayscale(1) contrast(1.05)}
.duo::before{content:"";position:absolute;inset:0;
  background:#1f3a6b; mix-blend-mode:multiply;}   /* shadows */
.duo::after{content:"";position:absolute;inset:0;
  background:#b5197a; mix-blend-mode:lighten;}     /* highlights */

Crush the photo to two colours where the second colour is the section (politics = blue, culture = magenta) — the Axios/Bloomberg wayfinding trick. How: grayscale the image, then a multiply layer tints the shadows and a lighten/screen layer tints the highlights. Recolour per section by swapping two hex values — no new image. Watch: lock the palette and reserve it for a tier of stories, or it stops meaning anything.

6 · Text behind the subject (depth headline)

MÅLET

cutout
/* 3 stacked layers: bg photo · headline · transparent cutout on top */
<div class="depth">
  <div class="bg"></div>          /* the photo */
  <h1>MÅLET</h1>                  /* real text, z between */
  <img class="cut" src="cutout.png"> /* from POST /v1/cutout */
</div>
.depth{position:relative}
.depth .bg,.depth img.cut{position:absolute;inset:0}
.depth h1{position:absolute;z-index:1}
.depth img.cut{z-index:2}        /* subject in front of the word */

The headline passes behind the person's head and shoulders — the most-shared "premium" trick of the last two years. How: stack three layers — background photo, then the headline, then a transparent cutout of the subject (the /v1/cutout PNG from §1 of this API) on top with a higher z-index. This is where the API and the CSS meet: the cutout above is a live BiRefNet cutout of the photo behind it. Watch: keep 60–70% of each word clear of the subject so the headline still reads.

7 · Weight-on-scroll headline

Rösterna räknas

/* type reacts to the reader — scroll-driven, no images */
@keyframes weigh{ to{ font-variation-settings:"wght" 900 } }
h1.scrolly{
  animation: weigh linear both;
  animation-timeline: view();   /* Interop 2026 */
}
@media (prefers-reduced-motion:reduce){ h1.scrolly{animation:none} }

The headline morphs thin → black as you scroll past it (scroll up and down to see it breathe). How: a keyframe animates font-variation-settings (a variable font) or font-weight, driven by animation-timeline: view() so the reader's scroll is the timeline — no JS. Watch: features only, and always give a static fallback via prefers-reduced-motion (done above).

8 · Before / after slider

after
before
/* drag the handle — comparison is an argument */
.slider .after{position:absolute;inset:0;
  clip-path:inset(0 calc(100% - var(--p,55%)) 0 0);}  /* reveal width */
<input type="range" oninput="
  this.closest('.slider').style.setProperty('--p',this.value+'%')">

Two versions of one photo with a draggable seam (före | efter, edit | original, lag A | lag B). How: stack the "after" copy over the "before", then a range input writes a CSS variable that drives a clip-path: inset() reveal — one line of inline JS, no library. Watch: label both sides or the comparison is ambiguous.

9 · Scanlines / CRT overlay (pure CSS)

scanlines
/* "archive / surveillance / broadcast" mood in one line */
.scan::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:repeating-linear-gradient(to bottom,
    rgba(0,0,0,.28) 0 1px, transparent 1px 3px);}

Horizontal CRT rows signal "caught on camera", retro, or surveillance. How: a single repeating-linear-gradient on a ::after overlay — no image, no JS. Drift it with a slow keyframe for live VHS. Watch: keep it subtle (opacity ~.15–.28); it's seasoning, not the dish.

Typographic plays — text & type as the hero

10 · Hollow / outlined headline

OAVGJORT

/* transparent letters with an outline — photo shows in the counters */
.stroke h1{
  color: transparent;
  -webkit-text-stroke: 2.5px #fff;   /* the outline does the reading */
  font: 800 90px system-ui;
}

The inverse of knockout: letters are hollow with a crisp outline, so the photo reads inside and around the type. How: color:transparent + -webkit-text-stroke over the image. Watch: only works at large, bold weights — thin fonts lose the outline; pair with a slight scrim on busy photos.

11 · Self-contrasting headline (blend)

BREAKING

/* the headline inverts against whatever is behind it — always legible */
.blend h1{ color:#fff; mix-blend-mode: difference; }

White text set to mix-blend-mode:difference inverts against the pixels beneath, so it stays readable over light and dark areas with no scrim. How: one property; the browser composites the headline against the photo. Watch: the colour shifts with the image — great for auto-contrast, less so when you need an exact brand colour.

12 · Text wraps around the subject

cutout

Reportageblocket flödar runt motivets verkliga silhuett i stället för en fyrkantig bild. Texten följer konturen av den utklippta personen — en tidskriftslayout rakt i flödet, utan manuell radbrytning. Detta binder ihop bild och brödtext till en komposition: ögat läser personen och stycket som en enhet, precis som i ett magasinuppslag. Effekten skalar med spaltbredden och faller tillbaka till en vanlig vänsterflyt om webbläsaren saknar stöd.

/* body text flows around the cutout's real silhouette */
img.float{
  float: left;
  shape-outside: url(cutout.png);   /* uses the PNG's alpha = the silhouette */
  shape-margin: 14px;
}

Body copy hugs the cut-out person instead of a rectangle — pure magazine. API tie-in again: shape-outside reads the alpha channel of the /v1/cutout PNG, so the text follows the actual silhouette. Watch: the cutout must be same-origin; keep a normal float fallback for old browsers.

13 · Oversized numeral

3

Hien om landslagsdrömmen

/* a giant translucent figure as graphic texture behind the label */
.num .big{position:absolute;font:900 300px system-ui;
  color:rgba(255,255,255,.17);}   /* rank / count / score as design */

A huge semi-transparent numeral turns a rank, score or count into the composition (lists, election seats, sports results, "del 3"). How: an absolutely-positioned oversized figure at low opacity behind the real headline. Watch: let it bleed off an edge — a contained number looks like a typo, a cropped one looks designed.

14 · Vertical / spine label

EXKLUSIVT

/* headline runs up the edge, magazine-spine style */
.vert h1{ writing-mode: vertical-rl; }

A kicker or section label set vertically up the photo's edge — frees the whole frame for the image and reads as "premium magazine". How: writing-mode: vertical-rl (add rotate for bottom-up). Watch: short words only; vertical body text is unreadable.

15 · Highlighted keyword

Årets festival blev en rekordsuccé

/* marker swipe on the one word that carries the story */
mark{ background:#b5197a; color:#fff;
  box-decoration-break: clone;   /* swipe survives line-wraps */
  padding:1px 5px; }

One word gets a marker/plate in the section colour — the eye lands on the point before reading the rest. How: wrap the keyword in <mark>; box-decoration-break:clone keeps the highlight clean when the word wraps. Watch: exactly one highlight per headline, chosen deliberately.

16 · Pull-quote over a portrait

Jag spelade för mardrömmen att aldrig få chansen.
/* oversized quote mark as a graphic, quote over the face's negative space */
.quote::before{ content:"\201D"; position:absolute;
  font:900 220px Georgia,serif; color:rgba(255,255,255,.2); }

A spoken line set big over the subject, anchored by a giant decorative quotation mark. How: a ::before glyph as texture + an italic serif blockquote placed in the photo's calm area. Watch: keep the quote off the face; lean on the portrait's negative space.

17 · Gradient-clip headline (neon energy)

MARKNADEN

/* fill the type with a gradient instead of a photo — markets/nightlife energy */
.neon h1{
  background: linear-gradient(90deg,#7be0a3,#5cc8ff,#b5197a);
  -webkit-background-clip: text; background-clip: text;
  color: transparent;
}

Same clip trick as knockout, but the fill is a gradient — high-energy for economy/markets, culture and nightlife. How: a linear-gradient background clipped to the text, over a darkened photo so it glows. Watch: energy signal — keep it off hard news and tragedy (same tone-match rule as the sticker/glitch effects).

Avoiding the two classic mistakes.
1 · Clipping. Size the type to the box, not the viewport — a vw headline keeps growing past a fixed-width card on big screens until the last letter vanishes under overflow:hidden (the bug that ate the "N" in the neon demo). Fixes: use container-query units so the headline scales to its container — font-size:clamp(26px,13cqi,72px) on a box with container-type:inline-size — or cap the max size; add inner padding so glyphs never touch the edge; keep line-height ≥ 1.1 (a tighter line under overflow:hidden shaves ascenders/descenders — including å/ä/ö dots); and always test narrow → wide.
2 · Contrast. Never drop raw text on an unknown photo — guarantee a readability floor: a scrim gradient (§1), a solid colour plate (§2), a text-stroke (§10), a text-shadow, or mix-blend-mode:difference (§11, auto-contrast). When you can't predict the image, the colour plate is bulletproof — it carries the text on any photo.
Server vs. browser. Everything above §11 is the API baking pixels (durable share-cards, reels). §11 is the feed compositing live in CSS over the served photo. Rule of thumb: bake it server-side when it must survive as a file (social cards, MP4); do it in CSS when it should stay real text, recolour per section, or react to the reader.