SurveyStream · Production SVG
Every curve is an exact arc-to-cubic conversion, every glyph is a real Archivo Bold
outline, and the whole thing is fitted to viewBox="0 0 600 140" with the bounds
computed rather than eyeballed — verified zero clipping on all four edges.
01 — The lockup
02 — The code
The mark's path data is short enough to read — that's the point of exact bezier conversion rather than auto-trace. The two wordmark paths are long because they are genuine glyph outlines; they're abbreviated here and complete in the file.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 140" width="600" height="140" role="img" aria-labelledby="ssTitle ssDesc" fill="none"> <title id="ssTitle">SurveyStream</title> <desc id="ssDesc">Horizontal logo lockup: an interlocking S-curve mark with an active flow accent, followed by the SurveyStream wordmark.</desc> <defs> <linearGradient id="ssFlow" gradientUnits="userSpaceOnUse" x1="12" y1="106.07" x2="79.79" y2="33.93"> <stop offset="0" stop-color="#00E5A3"/> <stop offset="1" stop-color="#06B6D4"/> </linearGradient> </defs> <g id="surveystream-lockup"> <g id="mark" stroke-linecap="round" stroke-linejoin="round"> <g id="mark-loop" stroke="#0F172A" stroke-width="10.08"> <path id="mark-loop-upper" d="M64.23 44.94C57.59 39.27 45.98 37.39 36.18 40.39C26.38 43.39 20.96 50.49 23.08 57.54C25.2 64.58 34.31 69.74 45.08 69.99"/> <path id="mark-loop-lower" d="M27.56 95.06C34.2 100.73 45.81 102.61 55.61 99.61C65.41 96.61 70.83 89.51 68.71 82.46C66.59 75.42 57.49 70.26 46.71 70.01"/> </g> <path id="mark-flow" d="M18.35 42.46C13.12 49.32 12.68 57.69 17.19 64.84C21.7 71.99 30.58 77 40.92 78.24C32.74 77.26 32.74 62.74 40.92 61.76C51.46 60.5 62.13 63.32 69.34 69.27C76.55 75.22 79.33 83.5 76.75 91.33" stroke="url(#ssFlow)" stroke-width="4.19"/> </g> <g id="wordmark" stroke="none"> <path id="wordmark-survey" fill="#0F172A" d="M129.97 97.05Q123.88 97.05 118.7 95.37Q113.51 93.7 110.39 90.08Q107.27 86.46 107.27 80.59Q107.27… <!-- 2834 chars of outline data, complete in the file -->"/> <path id="wordmark-stream" fill="#06B6D4" d="M366.03 97.05Q359.94 97.05 354.76 95.37Q349.58 93.7 346.45 90.08Q343.33 86.46 343.33 80.59Q343.3… <!-- 3608 chars of outline data, complete in the file -->"/> </g> </g> </svg>
03 — Geometry
Arc → cubic, exactly
Each bowl is an ellipse arc converted to cubic beziers by the standard construction, split into three ≤90° spans:
P(t) = (cx + rx·cos t, cy + ry·sin t) P'(t) = (−rx·sin t, ry·cos t) α = 4/3 · tan(Δt/4) C₁ = P(t₁) + α·P'(t₁) C₂ = P(t₂) − α·P'(t₂)
Maximum radial error for a 90° span is about 0.027% of the radius. At the size this mark is ever printed, that is well under a thousandth of a millimetre — mathematically clean anchor points with tangent continuity at every join.
Construction
rx 30 / ry 20,
centred (0, ∓20), each sweeping 230° from −38° to −268°. They touch at the
origin, which is where the stroke hands off.rx > ry gives the S a 0.78 width-to-height aspect.
Circular bowls give 0.50, which reads spindly next to a Bold wordmark.On the previous version of this mark I tried four interlocking constructions and abandoned all of them: overlapping gapped rings, an overshoot self-crossing, a true over/under weave, and a wide-bowl variant. Every one left a visible lump where two strokes of the same weight and colour met, or stopped reading as an S. Splitting the palette into a heavy slate base and a thin mint/cyan accent — your brief's structure — removes the problem entirely: a 4.2-unit accent crossing a 10.1-unit base reads as one passing over the other with no ambiguity to resolve.
Your brief asks for the loop "blended with an active data stream/pulse waveform". I built seven versions — sharp ECG zigzags, smooth decaying sine tails at both terminals, tapered arc dashes, and discrete pulse packets — and rendered every one. The zigzags and sine tails looped back on themselves into a knot at the accent's terminal, because the tangent there is steep relative to any wavelength short enough to fit. The dashes and dots were invisible at logo scale, adding file size and nothing else.
What shipped treats the flow accent itself as the data stream —
a line threading the loop and swapping sides. If you want a literal waveform, it needs to live
somewhere with room to breathe: a horizontal lockup variant with the wave running along the
baseline under the wordmark, or an animated web-only version where the accent's
stroke-dashoffset travels. Both are separate pieces of work, and I'd rather say
so than ship the knot.
04 — Colour
| Role | Value | On white | On slate | Verdict as text |
|---|---|---|---|---|
| Deep Slate | #0F172A | 17.85:1 | 1.00:1 | AAA on white |
| Cyan | #06B6D4 | 2.43:1 | 7.35:1 | Fails on white · AAA on slate |
| Mint | #00E5A3 | 1.65:1 | 10.83:1 | Fails on white · AAA on slate |
The precise position: WCAG 2.1 SC 1.4.3 explicitly exempts logotypes — "text that is part of a logo or brand name has no contrast requirement" — so this is not a conformance failure, and you can ship it. But an exemption is not the same as legibility. At the 96 px minimum width the cyan half of the wordmark visibly recedes against the slate half.
That's why lockup-600x140-slate.svg exists: identical geometry, wordmark
entirely in Deep Slate. Use the two-tone version at display sizes and on dark grounds where
cyan measures 7.35:1; use the slate version in small, dense, or print contexts. Reversed on
slate, both halves are comfortably above AAA.
These are your specified values and I've built to them exactly. For the record, they are not the palette from the colour system:
#04171F → #0F172A — ground moves from teal-navy to blue-slate#10BBD1 → #06B6D4 — cyan gets slightly deeper and bluer#2FE3A2 → #00E5A3 — mint gets more saturatedThe blue-slate shift is the substantive one: it breaks the teal bias that tied the neutrals to the accent across the whole system. Worth a deliberate decision about which palette is canonical before either ships, rather than discovering the split later in a component library.
05 — Fit and import
| Measure | Value | Notes |
|---|---|---|
| Mark bounds | x 12 → 79.79 | Left edge exactly on the 12-unit padding. |
| y 33.93 → 106.07 | Vertically centred in the 140-unit box. | |
| Wordmark bounds | x 103.84 → 588 | Right edge exactly on the padding. Baseline y 96.14. |
| Drawn content | 576 × 72.14 | Width-constrained fit at scale 0.7620. |
| Loop stroke | 10.08 | Matches the Archivo Bold stem weight. |
| Flow stroke | 4.19 | Thin enough that the crossing reads unambiguously. |
A 600×140 frame is 4.29:1. This lockup's natural ratio is 7.98:1, so fitting it to your box is width-constrained and leaves roughly 34 units of air above and below. That is correct behaviour, not a bug — I did not enlarge the mark to fill the frame, because the mark-to-wordmark ratio is a brand constant and stretching it to suit one canvas is how lockups drift.
If you want it tight-cropped instead, use
viewBox="0 0 600 84.14" with the same paths, or the existing
lockup-horizontal.svg which is already cropped to its own bounds.
mark-loop,
mark-flow, wordmark-survey, wordmark-stream. The
gradient lands as a real gradient fill on the stroke. If you need fills only, select
#mark and use Outline Stroke.width/height attributes are
there for Figma's benefit. For fluid web use, drop them and let CSS drive:
svg{width:100%;height:auto}. The default
preserveAspectRatio already centres correctly.userSpaceOnUse, with coordinates matched to the
mark's real bounding box — so it renders identically regardless of container size, and
won't skew the way objectBoundingBox does on a stroked path.ids if the page carries
more than one copy, or the url(#ssFlow) reference will bind to the first.