Hero: navnet delt av scrollen over en lynlinje; bildeserier
Deploy instance / deploy (push) Successful in 3s
lint-and-reload / lint (push) Successful in 2s

hero.js er forsidens hero (site.yaml hero.kind: module): Westra og
Engineering satt som i logoen, serif i kapiteler, og en lynlinje
(gpupaper's light_line.frag, uendret, i ordenes blå) tegnet over hele
den klebrige headeren på en WebGL-canvas, siktet inn under ordene der
logoens bånd går. Scrollen er tidslinjen: Westra glir ut til venstre
og Engineering til høyre, med litt etterslep. Logo-bildet skjules på
forsiden; hvem som er inne står øverst til høyre.

Åpningsspørsmålet sier "vi" i stedet for å gjenta navnet under det.

Bildeserier (slideshow) på forsiden, i headeren under navnet, og på
/prosjekter med alle prosjektbildene; bildene som lå inne i kortene
på /prosjekter er tatt ut, serien er galleriet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
blandClaude Fable 5.1 committed 2026-10-21 14:52:54 +02:00
1 parent 3ea125f56d
commit 2f3511dfb4
4 files changed
+693 -27

No files matched your search

+605
View File
@@ -0,0 +1,605 @@
// Westra Engineering's hero: the name, parted by the scroll, over a
// lightning line drawn full-screen on a sticky canvas.
//
// Content-owned: portal knows only site.yaml's `hero: {kind: module,
// module: hero.js}` and the contract - `mount(container) -> handle`
// with `stop()` - and serves this file same-origin at /site/hero.js.
// The module owns its look. The stylesheet it injects below restyles
// portal's .hero-module header the way uhhm's hero does: sticky at
// the viewport top for the whole page, so the line keeps running
// behind the translucent cards that scroll over it. On the landing
// page the name drawn here is the wordmark, so the image one is
// hidden there.
//
// Scroll is the timeline. Over the first SPLIT_RANGE of a viewport
// "Westra" slides left and "Engineering" right, each until it has
// left the screen, the way the logo already sets them: Westra to the
// left, Engineering to the right. The logo's ribbon and tagline are
// not drawn: the line takes the ribbon's place under the two words.
// The transform follows the scroll
// position through a short exponential lag (TAU_MS), so a wheel notch
// reads as a movement, not a jump; with reduced motion asked for it
// follows exactly. The copy under the name fades over the same range
// so it does not ghost through the cards.
//
// The line is /usr/share/gpupaper/shaders/light_line.frag, Brandon
// Fogerty's "Lightning" in glslsandbox conventions (time, mouse,
// resolution), the one gpupaper draws on the desktop. It is embedded
// verbatim; the two edits made to it at compile time are listed and
// explained at FRAG_EDITS. WebGL 1 is enough for it and is everywhere
// (a CSS paint worklet is not: no shaders, no per-pixel access, and
// Chromium only). Without WebGL the name and the scroll still work
// over plain paper.
const STYLE_ID = 'westra-hero-style';
// Viewport heights of scroll over which the name parts completely.
const SPLIT_RANGE = 0.6;
// How far the transform lags the scroll, as the time constant of an
// exponential approach: after one TAU it has closed 63% of the gap.
const TAU_MS = 140;
// The shader is cheap per pixel but runs for every pixel, three times
// over (its loop), and a 4K screen at a device pixel ratio of 2 is 33
// million pixels a frame: choppy on an integrated GPU. The line is a
// glow, so it is drawn at no more than this many pixels and the
// browser scales the canvas up to the screen; at 1080p that is about
// 0.7 of the screen's pixels, at 4K about a third.
const MAX_PIXELS = 1.2e6;
// /usr/share/gpupaper/shaders/light_line.frag, as is.
const LIGHT_LINE_FRAG = `// Lightning
// By: Brandon Fogerty
// bfogerty at gmail dot com
// xdpixel.com
#ifdef GL_ES
precision mediump float;
#endif
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
float Hash( vec2 p)
{
vec3 p2 = vec3(p.xy,2.0);
return fract(sin(dot(p2,vec3(27.1,20.7, 2.4)))*0.5453123);
}
float noise(in vec2 p)
{
vec2 i = floor(p);
vec2 f = fract(p);
f *= f * (3.0-2.0*f);
return mix(mix(Hash(i + vec2(0.,0.)), Hash(i + vec2(1.,0.)),f.x),
mix(Hash(i + vec2(0.,1.)), Hash(i + vec2(1.,1.)),f.x),
f.y);
}
float fbm(vec2 p)
{
float v = 0.0;
v += noise(p*1.0) * .5;
v += noise(p*2.) * .25;
v += noise(p*4.) * .15;
return v;
}
void main( void )
{
vec2 uv = ( gl_FragCoord.xy / resolution.xy ) * 2.0 - 1.0;
vec2 center = vec2(0.0, 0.0);
float angle = time * 0.4;
//uv.x = cos(angle) * (uv.x - center.x) - sin(angle) * (uv.y - center.y) + center.x;
//uv.y = sin(angle) * (uv.x - center.y) + cos(angle) * (uv.y - center.y) + center.y;
vec3 finalColor = vec3( 0.0 );
for( int i=0; i < 3; ++i )
{
float t = abs( 1.4 / (sin(uv.y + fbm( uv + time * 0.04 )) * 100.0) );
finalColor += t * vec3( 2.9, 1.5, .25 );
finalColor = vec3(t);
}
gl_FragColor = vec4( finalColor, 1.0 );
}
`;
// The edits, as exact replacements, so the source above stays a copy
// of the file. Each must match or the line is not drawn at all.
//
// 1. mediump → highp where the GPU has it: the hash takes sin() of a
// large dot product, which at 16-bit precision bands into stripes
// on phones. The two uniforms the second edit uses go in here too,
// after the precision statement, where GLSL ES wants them.
// 2. One line after uv is set: uv.y is shifted by `shift`, so the line
// (which the shader draws where uv.y + fbm crosses zero, about 45%
// below the middle of its own frame) lies under the two words, at
// the height of the logo's ribbon, wherever the layout has put
// them (see size()). The shader's own wander stays.
// 3. The last line. The file paints the line white on black, hot in
// the middle. Here it is one blue, the words' own (--accent), with
// the line's brightness as alpha, so the paper shows through
// everywhere else and the middle is that same blue, fully opaque.
const FRAG_EDITS = [
[
'#ifdef GL_ES\nprecision mediump float;\n#endif\n',
'#ifdef GL_ES\nprecision PRECISION float;\n#endif\nuniform vec3 tint;\nuniform float shift;\n',
],
[
'\tvec2 uv = ( gl_FragCoord.xy / resolution.xy ) * 2.0 - 1.0;\n',
'\tvec2 uv = ( gl_FragCoord.xy / resolution.xy ) * 2.0 - 1.0;\n\tuv.y -= shift;\n',
],
[
'\tgl_FragColor = vec4( finalColor, 1.0 );\n',
'\tfloat a = clamp( finalColor.r, 0.0, 1.0 );\n' + '\tgl_FragColor = vec4( tint * a, a );\n',
],
];
const VERT = 'attribute vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }';
const CSS = `
.hero-module {
/* Sticky at the viewport top for the whole scroll (its containing
block is the page itself), z-index 0 so the cards (z-index 1)
stack over it. The height is a floor, frozen to a measured pixel
value by the module once it runs (see size()); the svh ladder is
the first paint and the no-JS fallback. A floor and not a height,
so a page whose strip and copy need more than a screen (a phone)
grows rather than clips, and the cards start under it. */
position: sticky;
top: 0;
z-index: 0;
max-width: none;
width: 100%;
min-height: 100vh;
min-height: 100svh;
padding: 0;
justify-content: flex-end;
overflow: hidden;
}
/* The piece is whatever the header has left above the copy, not
portal's 55svh reservation; the name centres in it. It keeps a
quarter of the screen at least, so a strip and a long copy cannot
squeeze the name out. */
.hero-module .hero-piece {
position: relative;
flex: 1 1 auto;
width: 100%;
min-height: 26svh;
}
/* With a picture strip (slideshow): the name and the line take the
top, the strip runs the width under them, the question under that,
all in the one sticky header. The wordmark's bar is empty here (who
is here went to the corner), so it takes no room; the strip keeps
to its own height but never more than two fifths of the screen. */
.hero-module .hero-bar {
padding: 0;
}
.hero-module .slideshow {
flex: none;
margin: 0 0 1.25rem;
max-height: 40svh;
}
.hero-module.hero-slides .hero-copy {
padding-bottom: 2.5rem;
}
/* The name drawn below is the wordmark here. Who is here (portal
0.6: on the wordmark's own line, .hero-top, which would sit level
with the mark) goes to the top right corner of the header instead,
the sticky header being its containing block now that the copy is
not positioned. */
.hero-module .wordmark {
display: none;
}
.hero-module .hero-top {
position: absolute;
top: 0.9rem;
right: 1.5rem;
width: auto;
justify-content: flex-end;
}
.hero-module .hero-top .session {
position: static;
transform: none;
}
.hero-module .hero-copy {
position: static;
padding: 0 1.5rem 3.5rem;
gap: 0.6rem;
text-shadow: 0 0.12em 1.4em var(--hero-glow);
}
/* Behind everything in the header (the header is a stacking context
of its own, so -1 is still above the page). */
.westra-line {
position: absolute;
inset: 0;
z-index: -1;
width: 100%;
height: 100%;
display: block;
pointer-events: none;
}
/* The name, laid out as the logo lays it out: a box with the logo's
proportions (its 430x128 viewBox), Westra large at the top left,
Engineering under it and to the right. Positions and sizes are the
logo's own numbers as hundredths of the box width (cqw), so the
layout is the logo's at every size; the ribbon's place under both
words is where the line is aimed (see aim()). --p is the parting:
Westra leaves by half the viewport plus its own width, Engineering
the other way. */
.westra-name {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 1.5rem;
box-sizing: border-box;
container-type: size;
pointer-events: none;
--p: 0;
}
/* As wide as the piece allows, and no taller than most of it. */
.westra-mark {
position: relative;
width: min(100%, 40rem, 260cqh);
aspect-ratio: 430 / 128;
container-type: inline-size;
font-family: Georgia, 'Times New Roman', 'Liberation Serif', serif;
font-variant: small-caps;
font-feature-settings: 'smcp' 1;
font-weight: 600;
letter-spacing: 0.028em;
line-height: 1;
color: var(--accent);
}
.westra-mark > * {
position: absolute;
white-space: nowrap;
}
/* A halo of the paper's own colour around the glyphs, dense close in
and fading out, so the line's glow does not blur the words where it
runs under or through them. */
.westra-mark .westra-a,
.westra-mark .westra-b {
will-change: transform;
text-shadow:
0 0 0.04em var(--paper),
0 0 0.1em var(--paper),
0 0 0.2em var(--paper),
0 0 0.4em var(--paper),
0 0.06em 0.9em var(--hero-glow);
}
/* x 6, baseline 54, size 54 */
.westra-mark .westra-a {
left: 1.4cqw;
top: 2.8cqw;
font-size: 12.56cqw;
transform: translateX(calc(var(--p) * (-50vw - 100%)));
}
/* x 150, baseline 96, size 41 */
.westra-mark .westra-b {
left: 34.9cqw;
top: 14.8cqw;
font-size: 9.53cqw;
transform: translateX(calc(var(--p) * (50vw + 100%)));
}
`;
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = CSS;
document.head.appendChild(style);
}
const clamp01 = (x) => Math.min(1, Math.max(0, x));
// Gentle in, gentle out: the halves start moving as the scroll does,
// without a kick.
const ease = (p) => (p < 0.5 ? 2 * p * p : 1 - 2 * (1 - p) * (1 - p));
// "#rgb" / "#rrggbb" from a custom property, as a 0..1 triple.
function rgb(value, fallback) {
const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec((value || '').trim());
if (!m) return fallback;
let h = m[1];
if (h.length === 3) h = h.replace(/./g, (c) => c + c);
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
}
class WestraHero {
constructor(container) {
ensureStyle();
this.stopped = false;
this.hero = container.closest('.hero') || container.parentElement;
this.copy = this.hero ? this.hero.querySelector('.hero-copy') : null;
this.reduced = window.matchMedia('(prefers-reduced-motion: reduce)');
this.dark = window.matchMedia('(prefers-color-scheme: dark)');
this.canvas = document.createElement('canvas');
this.canvas.className = 'westra-line';
this.name = document.createElement('div');
this.name.className = 'westra-name';
const mark = document.createElement('div');
mark.className = 'westra-mark';
const a = document.createElement('span');
a.className = 'westra-a';
a.textContent = 'Westra';
const b = document.createElement('span');
b.className = 'westra-b';
b.textContent = 'Engineering';
mark.append(a, b);
this.name.append(mark);
this.mark = mark;
// The canvas covers the whole sticky header, under everything
// in it; the name sits in the piece, the space above the copy.
(this.hero || container).prepend(this.canvas);
container.append(this.name);
this.width = 0;
this.height = 1;
this.boxHeight = 1;
this.progress = 0;
this.t0 = performance.now();
this.last = this.t0;
this.raf = 0;
this.gl = this.initGl();
this.size();
this._onResize = () => this.onResize();
this._onScroll = () => this.wake();
this._onTheme = () => {
this.recolor();
this.wake();
};
this._onLost = (e) => {
e.preventDefault();
this.gl = null;
};
this._onRestored = () => {
this.gl = this.initGl();
this.size();
this.wake();
};
window.addEventListener('resize', this._onResize);
window.addEventListener('scroll', this._onScroll, { passive: true });
if (this.hero && typeof ResizeObserver === 'function') {
this.observer = new ResizeObserver(() => {
if (this.stopped) return;
if (this.hero.offsetHeight !== this.boxHeight) {
this.fit();
this.wake();
}
});
this.observer.observe(this.hero);
}
this.dark.addEventListener('change', this._onTheme);
this.reduced.addEventListener('change', this._onTheme);
this.canvas.addEventListener('webglcontextlost', this._onLost);
this.canvas.addEventListener('webglcontextrestored', this._onRestored);
this.wake();
}
// The frame loop runs while the line animates; with reduced motion
// the line stands still and frames are drawn only when the scroll,
// size or theme changes, until the transform has settled.
wake() {
if (this.stopped || this.raf) return;
this.raf = requestAnimationFrame((now) => this.frame(now));
}
frame(now) {
this.raf = 0;
if (this.stopped) return;
const dt = Math.min(100, now - this.last);
this.last = now;
const target = clamp01(window.scrollY / (this.height * SPLIT_RANGE));
if (this.reduced.matches) {
this.progress = target;
} else {
this.progress += (target - this.progress) * (1 - Math.exp(-dt / TAU_MS));
if (Math.abs(target - this.progress) < 0.0005) this.progress = target;
}
const p = ease(this.progress);
this.name.style.setProperty('--p', p.toFixed(4));
// The copy goes sooner than the name: it has to be gone before
// the first card reaches it, on a short screen too.
if (this.copy) {
const visible = 1 - clamp01(this.progress * 2.5);
this.copy.style.opacity = visible.toFixed(3);
this.copy.style.visibility = visible <= 0.01 ? 'hidden' : '';
}
this.draw(this.reduced.matches ? 0 : (now - this.t0) / 1000);
const settled = this.progress === target;
if (!this.reduced.matches || !settled) this.wake();
}
// The header's height is frozen from one measurement of the
// viewport rather than left to svh: on phones the box would
// otherwise follow the address bar in and out and the name with
// it. Re-measured when the width changes (a rotation, a window
// resize), not when only the height moves a little (that bar).
onResize() {
const dw = window.innerWidth !== this.width;
const dh = Math.abs(window.innerHeight - this.height) > this.height * 0.25;
if (dw || dh) this.size();
this.wake();
}
size() {
this.width = window.innerWidth;
this.height = Math.max(1, window.innerHeight);
if (this.hero) this.hero.style.minHeight = this.height + 'px';
this.fit();
}
// The canvas covers the header's real box, which is the screen
// unless the strip and the copy need more; measured, since the
// copy's height is the fonts' and the strip's is its own.
fit() {
this.boxHeight = Math.max(1, this.hero ? this.hero.offsetHeight : this.height);
const dpr = window.devicePixelRatio || 1;
const scale = Math.min(dpr, Math.sqrt(MAX_PIXELS / (this.width * this.boxHeight)));
this.canvas.width = Math.max(1, Math.round(this.width * scale));
this.canvas.height = Math.max(1, Math.round(this.boxHeight * scale));
if (this.gl) {
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
this.aim();
}
}
// Where the logo's ribbon would be on screen (its lower edge, y
// 116 of the box's 128), as the shader's uv.y (+1 at the top, -1
// at the foot): the line's own zero crossing sits about 0.45
// below its frame's middle, so the frame is shifted up by that
// much less.
aim() {
const hero = this.hero ? this.hero.getBoundingClientRect() : { top: 0 };
const mark = this.mark.getBoundingClientRect();
const ribbonY = mark.top - hero.top + mark.height * (116 / 128);
const uvY = 1 - 2 * (ribbonY / this.boxHeight);
this.gl.uniform1f(this.u.shift, uvY + 0.45);
}
initGl() {
const gl = this.canvas.getContext('webgl', {
alpha: true,
antialias: false,
depth: false,
stencil: false,
premultipliedAlpha: true,
powerPreference: 'low-power',
});
if (!gl) return null;
const highp = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
const precision = highp && highp.precision > 0 ? 'highp' : 'mediump';
let frag = LIGHT_LINE_FRAG;
for (const [from, to] of FRAG_EDITS) {
if (!frag.includes(from)) {
console.warn('westra hero: light_line.frag no longer matches its edits; line not drawn');
return null;
}
frag = frag.replace(from, to.replace('PRECISION', precision));
}
const compile = (type, src) => {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
console.warn('westra hero: shader', gl.getShaderInfoLog(s));
return null;
}
return s;
};
const vs = compile(gl.VERTEX_SHADER, VERT);
const fs = compile(gl.FRAGMENT_SHADER, frag);
if (!vs || !fs) return null;
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.warn('westra hero: link', gl.getProgramInfoLog(program));
return null;
}
gl.useProgram(program);
// One triangle that covers the clip space; the shader only
// reads gl_FragCoord.
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const position = gl.getAttribLocation(program, 'position');
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
this.u = {
time: gl.getUniformLocation(program, 'time'),
resolution: gl.getUniformLocation(program, 'resolution'),
tint: gl.getUniformLocation(program, 'tint'),
shift: gl.getUniformLocation(program, 'shift'),
};
gl.clearColor(0, 0, 0, 0);
this.gl = gl;
this.recolor();
this.aim();
return gl;
}
// The line is the words' blue: --accent from the page's own
// palette (custom.css sets it for both schemes), read when the
// scheme changes rather than every frame.
recolor() {
if (!this.gl) return;
const cs = getComputedStyle(this.hero || document.documentElement);
const tint = rgb(cs.getPropertyValue('--accent'), [0.13, 0.19, 0.31]);
this.gl.uniform3f(this.u.tint, tint[0], tint[1], tint[2]);
}
draw(time) {
const gl = this.gl;
if (!gl) return;
gl.uniform1f(this.u.time, time);
gl.uniform2f(this.u.resolution, this.canvas.width, this.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
stop() {
this.stopped = true;
if (this.raf) cancelAnimationFrame(this.raf);
window.removeEventListener('resize', this._onResize);
window.removeEventListener('scroll', this._onScroll);
if (this.observer) this.observer.disconnect();
this.dark.removeEventListener('change', this._onTheme);
this.reduced.removeEventListener('change', this._onTheme);
this.canvas.removeEventListener('webglcontextlost', this._onLost);
this.canvas.removeEventListener('webglcontextrestored', this._onRestored);
if (this.gl) {
const lose = this.gl.getExtension('WEBGL_lose_context');
if (lose) lose.loseContext();
}
if (this.hero) this.hero.style.minHeight = '';
if (this.copy) {
this.copy.style.opacity = '';
this.copy.style.visibility = '';
}
this.canvas.remove();
this.name.remove();
}
}
export function mount(container) {
return new WestraHero(container);
}
+31 -3
View File
@@ -2,7 +2,7 @@
# as intact as possible, in the portal's question/alternative form. # as intact as possible, in the portal's question/alternative form.
# Norwegian is the source language; the original site's wording is # Norwegian is the source language; the original site's wording is
# kept verbatim where it existed. # kept verbatim where it existed.
name: Hva kan Westra Engineering gjøre for prosjektet deres? name: Hva kan vi gjøre for prosjektet deres?
description: > description: >
Westra Engineering AS er et serviceorientert og fleksibelt Westra Engineering AS er et serviceorientert og fleksibelt
rådgiverfirma innen bygg- og anleggsbransjen, som tar seg av hele rådgiverfirma innen bygg- og anleggsbransjen, som tar seg av hele
@@ -11,6 +11,36 @@ description: >
responsible: responsible:
name: Jørn Wenger name: Jørn Wenger
contact: jorn@westra-engineering.no contact: jorn@westra-engineering.no
# Bildeserien: prosjektene tvers over skjermen, under navnet og linjen
# i heroen (hero.js). Bildene er de fra westra-engineering.no i sin
# egen størrelse; bildetekstene er prosjektenes navn fra /prosjekter.
slideshow:
max_height: 32
slides:
- image: images/aarvoll-flerbrukshall.jpg
width: 768
height: 576
caption: Årvoll Flerbrukshall — prosjekteringsledelse og byggeoppfølging
- image: images/edvard-griegs-vei.jpg
width: 640
height: 480
caption: Sykehjem i Edvard Griegs vei, Kolbotn — prosjektledelse og byggeoppfølging
- image: images/whru-eldfisk.jpg
width: 768
height: 576
caption: Waste Heat Recovery-unit på Eldfisk Ester — byggeoppfølging
- image: images/moylandsenteret.jpg
width: 768
height: 576
caption: Møylandsenteret i Andebu — prosjekteringsledelse og byggoppfølging
- image: images/m343-modul.jpg
width: 603
height: 452
caption: M343-modulen for Heidrun — strukturanalyse
- image: images/oppdriftstanker.jpg
width: 604
height: 402
caption: Oppdriftstanker for riser base — design
alternatives: alternatives:
- name: Hvorfor velge Westra Engineering som partner - name: Hvorfor velge Westra Engineering som partner
description: > description: >
@@ -88,8 +118,6 @@ alternatives:
description: > description: >
Prosjekteringsledelse og byggeoppfølging for formålsbygg, Prosjekteringsledelse og byggeoppfølging for formålsbygg,
bruer og offshore-relaterte konstruksjoner. bruer og offshore-relaterte konstruksjoner.
![Årvoll Flerbrukshall](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/aarvoll-flerbrukshall.jpg)
icon: lucide:hard-hat icon: lucide:hard-hat
- name: Ta kontakt - name: Ta kontakt
description: > description: >
+54 -24
View File
@@ -3,6 +3,60 @@ name: Hva har Westra Engineering bygget?
description: > description: >
Et utvalg gjennomførte prosjekter — landbaserte bygg, bru, og Et utvalg gjennomførte prosjekter — landbaserte bygg, bru, og
offshore-relaterte konstruksjoner. offshore-relaterte konstruksjoner.
# Bildeserien: alle prosjektbildene fra westra-engineering.no, i sidens
# rekkefølge, tvers over skjermen under spørsmålet. Nordenga bru har
# ikke noe bilde.
slideshow:
max_height: 50
slides:
- image: images/edvard-griegs-vei.jpg
width: 640
height: 480
caption: Sykehjem i Edvard Griegs vei, Kolbotn — prosjektledelse og byggeoppfølging
- image: images/aarvoll-flerbrukshall.jpg
width: 768
height: 576
caption: Årvoll Flerbrukshall — prosjekteringsledelse og byggeoppfølging
- image: images/moylandsenteret.jpg
width: 768
height: 576
caption: Møylandsenteret i Andebu, Sandefjord — prosjekteringsledelse og byggoppfølging
- image: images/aurskog-holand.jpg
width: 768
height: 576
caption: Aurskog-Høland helsesenter — prosjekteringsledelse og byggeoppfølging
- image: images/whru-eldfisk.jpg
width: 768
height: 576
caption: Waste Heat Recovery-unit på Eldfisk Ester, for ConocoPhillips — byggeoppfølging
- image: images/utlasting-whru-1.jpg
width: 768
height: 461
caption: Utlasting av WHRU for offshore-installasjon
- image: images/utlasting-whru-2.jpg
width: 768
height: 461
caption: Utlasting av WHRU, del to
- image: images/m343-modul.jpg
width: 603
height: 452
caption: M343-modulen for vanninjeksjon og sulfatfjerning — strukturanalyse, installert på Heidrun 2002
- image: images/c17-statfjord.jpg
width: 602
height: 397
caption: C17-modulen på Statfjord C — strukturanalyse 1997–1998
- image: images/ror-sjosikring.jpg
width: 602
height: 397
caption: Løfting og sjøsikring av rør for offshore-installasjon
- image: images/klammer-loft.jpg
width: 608
height: 397
caption: Klammer for offshore-løft
- image: images/oppdriftstanker.jpg
width: 604
height: 402
caption: Oppdriftstanker ved installasjon av offshore riser base
alternatives: alternatives:
- name: Landbaserte prosjekter - name: Landbaserte prosjekter
description: > description: >
@@ -14,26 +68,18 @@ alternatives:
- name: Sykehjem i Edvard Griegs vei, Kolbotn - name: Sykehjem i Edvard Griegs vei, Kolbotn
description: > description: >
Oppegård kommune. Prosjektledelse og byggeoppfølging. Oppegård kommune. Prosjektledelse og byggeoppfølging.
![Sykehjem i Edvard Griegs vei](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/edvard-griegs-vei.jpg)
icon: lucide:building icon: lucide:building
- name: Årvoll Flerbrukshall - name: Årvoll Flerbrukshall
description: > description: >
Prosjekteringsledelse og byggeoppfølging. Prosjekteringsledelse og byggeoppfølging.
![Årvoll Flerbrukshall](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/aarvoll-flerbrukshall.jpg)
icon: lucide:warehouse icon: lucide:warehouse
- name: Møylandsenteret i Andebu, Sandefjord kommune - name: Møylandsenteret i Andebu, Sandefjord kommune
description: > description: >
Prosjekteringsledelse og byggoppfølging. Prosjekteringsledelse og byggoppfølging.
![Møylandsenteret](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/moylandsenteret.jpg)
icon: lucide:building-2 icon: lucide:building-2
- name: Aurskog-Høland helsesenter - name: Aurskog-Høland helsesenter
description: > description: >
Prosjekteringsledelse og byggeoppfølging. Prosjekteringsledelse og byggeoppfølging.
![Aurskog-Høland helsesenter](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/aurskog-holand.jpg)
icon: lucide:hospital icon: lucide:hospital
- name: Nordenga Bru, Østre Tangent - name: Nordenga Bru, Østre Tangent
description: > description: >
@@ -48,16 +94,10 @@ alternatives:
- name: Waste Heat Recovery-unit på Eldfisk Ester - name: Waste Heat Recovery-unit på Eldfisk Ester
description: > description: >
Installert for ConocoPhillips. Byggeoppfølging. Installert for ConocoPhillips. Byggeoppfølging.
![WHRU på Eldfisk Ester](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/whru-eldfisk.jpg)
icon: lucide:flame icon: lucide:flame
- name: Utlasting og offshore-installasjon - name: Utlasting og offshore-installasjon
description: > description: >
Prosjekttjenester relatert til utlasting og offshore-installasjon. Prosjekttjenester relatert til utlasting og offshore-installasjon.
![Utlasting av WHRU](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/utlasting-whru-1.jpg)
![Utlasting av WHRU, del to](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/utlasting-whru-2.jpg)
icon: lucide:ship icon: lucide:ship
- name: Design og strukturanalyse av offshore-moduler - name: Design og strukturanalyse av offshore-moduler
description: > description: >
@@ -69,14 +109,10 @@ alternatives:
description: > description: >
Strukturanalyse, 2001. Modulen ble installert på Heidrun i Strukturanalyse, 2001. Modulen ble installert på Heidrun i
2002. 2002.
![M343-modulen](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/m343-modul.jpg)
icon: lucide:box icon: lucide:box
- name: C17-modulen på Statfjord C - name: C17-modulen på Statfjord C
description: > description: >
Strukturanalyse, 1997–1998. Strukturanalyse, 1997–1998.
![C17-modulen](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/c17-statfjord.jpg)
icon: lucide:box icon: lucide:box
- name: Strukturdesign av offshore-relatert utstyr - name: Strukturdesign av offshore-relatert utstyr
description: > description: >
@@ -88,21 +124,15 @@ alternatives:
description: > description: >
Design relatert til løfting og sjøsikring av rør for Design relatert til løfting og sjøsikring av rør for
offshore-installasjon. offshore-installasjon.
![Sjøsikring av rør](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/ror-sjosikring.jpg)
icon: lucide:cable icon: lucide:cable
- name: Klammer for offshore-løft - name: Klammer for offshore-løft
description: > description: >
Design av klammer for offshore-løft. Design av klammer for offshore-løft.
![Klammer for offshore-løft](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/klammer-loft.jpg)
icon: lucide:grip icon: lucide:grip
- name: Oppdriftstanker for riser base - name: Oppdriftstanker for riser base
description: > description: >
Design av oppdriftstanker ved installasjon av offshore riser Design av oppdriftstanker ved installasjon av offshore riser
base. base.
![Oppdriftstanker](https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/images/oppdriftstanker.jpg)
icon: lucide:database icon: lucide:database
- name: Tilbake - name: Tilbake
description: > description: >
+3
View File
@@ -3,3 +3,6 @@ wordmark: https://prosjekt.klingenbergbygg.no/westra/questions/raw/branch/main/i
wordmark_height: 3 wordmark_height: 3
lang: no lang: no
stylesheet: custom.css stylesheet: custom.css
hero:
kind: module
module: hero.js # the name over a lightning line, served at /site/hero.js